From c3c4e17b1374186674f19835896d0ce783b9b635 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Thu, 17 Apr 2025 17:28:34 +0100 Subject: [PATCH 01/10] remove gift --- .../TelegramEngine/Payments/StarGifts.swift | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 92e0f4a7ab..20e098199c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1459,10 +1459,10 @@ private final class ProfileGiftsContextImpl { return _internal_transferStarGift(account: self.account, prepaid: prepaid, reference: reference, peerId: peerId) } - func buyStarGift(reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal { - var gift = self.gifts.first(where: { $0.reference == reference }) + func buyStarGift(gift inputGift: TelegramCore.StarGift, peerId: EnginePeer.Id) -> Signal { + var gift = self.gifts.first(where: { $0.gift == inputGift }) if gift == nil { - gift = self.filteredGifts.first(where: { $0.reference == reference }) + gift = self.filteredGifts.first(where: { $0.gift == inputGift }) } guard case let .unique(uniqueGift) = gift?.gift else { return .complete() @@ -1471,13 +1471,19 @@ private final class ProfileGiftsContextImpl { if let count = self.count { self.count = max(0, count - 1) } - self.gifts.removeAll(where: { $0.reference == reference }) - self.filteredGifts.removeAll(where: { $0.reference == reference }) + self.gifts.removeAll(where: { $0.gift == inputGift }) + self.filteredGifts.removeAll(where: { $0.gift == inputGift }) self.pushState() return _internal_buyStarGift(account: self.account, slug: uniqueGift.slug, peerId: peerId) } + func removeStarGift(gift: TelegramCore.StarGift) { + self.gifts.removeAll(where: { $0.gift == gift }) + self.filteredGifts.removeAll(where: { $0.gift == gift }) + self.pushState() + } + func upgradeStarGift(formId: Int64?, reference: StarGiftReference, keepOriginalInfo: Bool) -> Signal { return Signal { [weak self] subscriber in guard let self else { @@ -1893,11 +1899,11 @@ public final class ProfileGiftsContext { } } - public func buyStarGift(reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal { + public func buyStarGift(gift: TelegramCore.StarGift, peerId: EnginePeer.Id) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.buyStarGift(reference: reference, peerId: peerId).start(error: { error in + disposable.set(impl.buyStarGift(gift: gift, peerId: peerId).start(error: { error in subscriber.putError(error) }, completed: { subscriber.putCompletion() @@ -1907,6 +1913,12 @@ public final class ProfileGiftsContext { } } + public func removeStarGift(gift: TelegramCore.StarGift) { + self.impl.with { impl in + impl.removeStarGift(gift: gift) + } + } + public func transferStarGift(prepaid: Bool, reference: StarGiftReference, peerId: EnginePeer.Id) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() From a8c7b217a425e01865669c4ebe8e1a30a8d4945b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 21 Apr 2025 17:02:37 +0400 Subject: [PATCH 02/10] Various improvements --- submodules/Camera/Sources/Camera.swift | 5 +- .../Sources/SheetComponent.swift | 7 +- submodules/TelegramApi/Sources/Api0.swift | 1 + submodules/TelegramApi/Sources/Api12.swift | 20 + submodules/TelegramApi/Sources/Api38.swift | 8 +- .../TelegramEngine/Payments/StarGifts.swift | 49 +- .../Payments/TelegramEnginePayments.swift | 4 +- .../Sources/GiftItemComponent.swift | 3 +- .../Sources/GiftSetupScreen.swift | 1 - .../Sources/GiftStoreScreen.swift | 270 ++++---- .../Sources/ButtonsComponent.swift | 6 +- .../Sources/GiftViewScreen.swift | 229 ++++--- .../MediaEditor/Sources/MediaEditor.swift | 8 +- .../Sources/MediaEditorRenderer.swift | 27 +- .../MediaEditorScreen/Sources/EditCover.swift | 45 -- .../Sources/EditStories.swift | 5 +- .../Sources/MediaEditorScreen.swift | 587 ++++++++++++++---- .../Sources/SelectionPanelComponent.swift | 99 ++- .../Sources/PeerInfoScreenAvatarSetup.swift | 7 +- .../Sources/PeerInfoGiftsPaneNode.swift | 6 +- .../ChatControllerOpenAttachmentMenu.swift | 12 +- .../Sources/SharedAccountContext.swift | 26 +- .../Sources/TelegramRootController.swift | 10 +- 23 files changed, 978 insertions(+), 457 deletions(-) delete mode 100644 submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditCover.swift 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 { From 2962851527c841ba49f4d4c4f2d574ce70790c81 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 22 Apr 2025 21:06:46 +0400 Subject: [PATCH 03/10] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 18 + .../AccountContext/Sources/Premium.swift | 6 + .../Sources/PremiumBoostLevelsScreen.swift | 14 + submodules/TelegramApi/Sources/Api0.swift | 4 + submodules/TelegramApi/Sources/Api3.swift | 22 + submodules/TelegramApi/Sources/Api36.swift | 88 ++-- submodules/TelegramApi/Sources/Api37.swift | 52 ++ submodules/TelegramApi/Sources/Api38.swift | 26 +- .../TelegramEngine/Messages/Stories.swift | 9 +- .../Payments/BotPaymentForm.swift | 6 + .../TelegramEngine/Payments/StarGifts.swift | 214 ++++++-- .../Payments/TelegramEnginePayments.swift | 2 +- .../Peers/ChannelAdminEventLogs.swift | 3 + .../CameraScreen/Sources/CameraScreen.swift | 6 +- .../ChatRecentActionsHistoryTransition.swift | 27 + .../Sources/GiftOptionsScreen.swift | 21 +- .../Sources/GiftSetupScreen.swift | 54 ++ .../Sources/GiftStoreScreen.swift | 88 ++-- .../Sources/GiftViewScreen.swift | 154 +++--- .../MediaEditor/Sources/MediaEditor.swift | 18 +- .../Sources/MediaEditorValues.swift | 2 +- .../Sources/MediaEditorScreen.swift | 484 ++++++++++-------- .../Sources/MediaScrubberComponent.swift | 92 ++++ .../Sources/PeerInfoScreen.swift | 51 ++ .../Sources/PeerInfoGiftsPaneNode.swift | 4 +- .../Sources/StarsTransactionScreen.swift | 82 ++- .../StarsTransactionsListPanelComponent.swift | 13 +- .../Menu/AutoTranslate.imageset/Contents.json | 12 + .../AutoTranslate.imageset/translation.pdf | Bin 0 -> 12400 bytes .../TelegramUI/Sources/AppDelegate.swift | 8 +- .../Sources/ApplicationContext.swift | 4 +- .../Sources/MakeTempAccountContext.swift | 2 +- .../Sources/NotificationContentContext.swift | 2 +- .../Sources/SharedAccountContext.swift | 6 +- 34 files changed, 1086 insertions(+), 508 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 74cf0c9035..16e056f9b0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14202,3 +14202,21 @@ Sorry for the inconvenience."; "Story.Privacy.KeepOnMyPageManyInfo" = "Keep these stories on your profile even after they expire in %@. Privacy settings will apply."; "Story.Privacy.KeepOnChannelPageManyInfo" = "Keep these stories on the channel profile even after they expire in %@."; "Story.Privacy.KeepOnGroupPageManyInfo" = "Keep these stories on the group page even after they expire in %@."; + +"Gift.Options.Gift.Filter.Resale" = "Resale"; +"Gift.Options.Gift.Resale" = "resale"; + +"Stars.Intro.Transaction.GiftPurchase" = "Gift Purchase"; +"Stars.Intro.Transaction.GiftSale" = "Gift Sale"; + +"Stars.Transaction.GiftPurchase" = "Gift Purchase"; +"Stars.Transaction.GiftSale" = "Gift Sale"; + +"Channel.Info.AutoTranslate" = "Auto-Translate Messages"; + +"ChannelBoost.Table.AutoTranslate" = "Autotranslation of Messages"; +"ChannelBoost.AutoTranslate" = "Autotranslation of Messages"; +"ChannelBoost.AutoTranslateLevelText" = "Your channel needs **Level %1$@** to enable autotranslation of messages."; + +"Channel.AdminLog.MessageToggleAutoTranslateOn" = "%@ enabled autotranslation of messages"; +"Channel.AdminLog.MessageToggleAutoTranslateOff" = "%@ disabled autotranslation of messages"; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 790a21b077..2dea082727 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -125,6 +125,7 @@ public enum BoostSubject: Equatable { case emojiPack case noAds case wearGift + case autoTranslate } public enum StarsPurchasePurpose: Equatable { @@ -164,6 +165,7 @@ public struct PremiumConfiguration { minChannelCustomWallpaperLevel: 10, minChannelRestrictAdsLevel: 50, minChannelWearGiftLevel: 8, + minChannelAutoTranslateLevel: 3, minGroupProfileIconLevel: 7, minGroupEmojiStatusLevel: 8, minGroupWallpaperLevel: 9, @@ -193,6 +195,7 @@ public struct PremiumConfiguration { public let minChannelCustomWallpaperLevel: Int32 public let minChannelRestrictAdsLevel: Int32 public let minChannelWearGiftLevel: Int32 + public let minChannelAutoTranslateLevel: Int32 public let minGroupProfileIconLevel: Int32 public let minGroupEmojiStatusLevel: Int32 public let minGroupWallpaperLevel: Int32 @@ -221,6 +224,7 @@ public struct PremiumConfiguration { minChannelCustomWallpaperLevel: Int32, minChannelRestrictAdsLevel: Int32, minChannelWearGiftLevel: Int32, + minChannelAutoTranslateLevel: Int32, minGroupProfileIconLevel: Int32, minGroupEmojiStatusLevel: Int32, minGroupWallpaperLevel: Int32, @@ -248,6 +252,7 @@ public struct PremiumConfiguration { self.minChannelCustomWallpaperLevel = minChannelCustomWallpaperLevel self.minChannelRestrictAdsLevel = minChannelRestrictAdsLevel self.minChannelWearGiftLevel = minChannelWearGiftLevel + self.minChannelAutoTranslateLevel = minChannelAutoTranslateLevel self.minGroupProfileIconLevel = minGroupProfileIconLevel self.minGroupEmojiStatusLevel = minGroupEmojiStatusLevel self.minGroupWallpaperLevel = minGroupWallpaperLevel @@ -283,6 +288,7 @@ public struct PremiumConfiguration { minChannelCustomWallpaperLevel: get(data["channel_custom_wallpaper_level_min"]) ?? defaultValue.minChannelCustomWallpaperLevel, minChannelRestrictAdsLevel: get(data["channel_restrict_sponsored_level_min"]) ?? defaultValue.minChannelRestrictAdsLevel, minChannelWearGiftLevel: get(data["channel_emoji_status_level_min"]) ?? defaultValue.minChannelWearGiftLevel, + minChannelAutoTranslateLevel: get(data["channel_autotranslation_level_min"]) ?? defaultValue.minChannelAutoTranslateLevel, minGroupProfileIconLevel: get(data["group_profile_bg_icon_level_min"]) ?? defaultValue.minGroupProfileIconLevel, minGroupEmojiStatusLevel: get(data["group_emoji_status_level_min"]) ?? defaultValue.minGroupEmojiStatusLevel, minGroupWallpaperLevel: get(data["group_wallpaper_level_min"]) ?? defaultValue.minGroupWallpaperLevel, diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index aae7f44fe6..d9c5782795 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -61,6 +61,8 @@ func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: Acco return configuration.minChannelRestrictAdsLevel case .wearGift: return configuration.minChannelWearGiftLevel + case .autoTranslate: + return configuration.minChannelAutoTranslateLevel } } @@ -243,6 +245,7 @@ private final class LevelSectionComponent: CombinedComponent { case emojiPack case noAds case wearGift + case autoTranslate func title(strings: PresentationStrings, isGroup: Bool) -> String { switch self { @@ -274,6 +277,8 @@ private final class LevelSectionComponent: CombinedComponent { return strings.ChannelBoost_Table_NoAds case .wearGift: return strings.ChannelBoost_Table_WearGift + case .autoTranslate: + return strings.ChannelBoost_Table_AutoTranslate } } @@ -307,6 +312,8 @@ private final class LevelSectionComponent: CombinedComponent { return "Premium/BoostPerk/NoAds" case .wearGift: return "Premium/BoostPerk/NoAds" + case .autoTranslate: + return "Chat/Title Panels/Translate" } } } @@ -647,6 +654,8 @@ private final class SheetContent: CombinedComponent { textString = strings.ChannelBoost_EnableNoAdsLevelText("\(requiredLevel)").string case .wearGift: textString = strings.ChannelBoost_WearGiftLevelText("\(requiredLevel)").string + case .autoTranslate: + textString = strings.ChannelBoost_AutoTranslateLevelText("\(requiredLevel)").string } } else { let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining)) @@ -1162,6 +1171,9 @@ private final class SheetContent: CombinedComponent { if !isGroup && level >= requiredBoostSubjectLevel(subject: .noAds, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.noAds) } + if !isGroup && level >= requiredBoostSubjectLevel(subject: .autoTranslate, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.autoTranslate) + } // if !isGroup && level >= requiredBoostSubjectLevel(subject: .wearGift, group: isGroup, context: component.context, configuration: premiumConfiguration) { // perks.append(.wearGift) // } @@ -1466,6 +1478,8 @@ private final class BoostLevelsContainerComponent: CombinedComponent { titleString = strings.ChannelBoost_NoAds case .wearGift: titleString = strings.ChannelBoost_WearGift + case .autoTranslate: + titleString = strings.ChannelBoost_AutoTranslate } } else { titleString = isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index a3ffedec81..6d25bfee71 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -171,6 +171,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[589338437] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStartGroupCall($0) } dict[-1895328189] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStopPoll($0) } dict[1693675004] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleAntiSpam($0) } + dict[-988285058] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleAutotranslation($0) } dict[46949251] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleForum($0) } dict[1456906823] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleGroupCallSetting($0) } dict[460916654] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleInvites($0) } @@ -1461,6 +1462,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) } dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } + dict[-1014513586] = { return Api.stories.CanSendStoryCount.parse_canSendStoryCount($0) } dict[-488736969] = { return Api.stories.FoundStories.parse_foundStories($0) } dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) } dict[1673780490] = { return Api.stories.Stories.parse_stories($0) } @@ -2592,6 +2594,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stories.AllStories: _1.serialize(buffer, boxed) + case let _1 as Api.stories.CanSendStoryCount: + _1.serialize(buffer, boxed) case let _1 as Api.stories.FoundStories: _1.serialize(buffer, boxed) case let _1 as Api.stories.PeerStories: diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index b6ec88ab5b..1818e0060b 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -605,6 +605,7 @@ public extension Api { case channelAdminLogEventActionStartGroupCall(call: Api.InputGroupCall) case channelAdminLogEventActionStopPoll(message: Api.Message) case channelAdminLogEventActionToggleAntiSpam(newValue: Api.Bool) + case channelAdminLogEventActionToggleAutotranslation(newValue: Api.Bool) case channelAdminLogEventActionToggleForum(newValue: Api.Bool) case channelAdminLogEventActionToggleGroupCallSetting(joinMuted: Api.Bool) case channelAdminLogEventActionToggleInvites(newValue: Api.Bool) @@ -897,6 +898,12 @@ public extension Api { } newValue.serialize(buffer, true) break + case .channelAdminLogEventActionToggleAutotranslation(let newValue): + if boxed { + buffer.appendInt32(-988285058) + } + newValue.serialize(buffer, true) + break case .channelAdminLogEventActionToggleForum(let newValue): if boxed { buffer.appendInt32(46949251) @@ -1039,6 +1046,8 @@ public extension Api { return ("channelAdminLogEventActionStopPoll", [("message", message as Any)]) case .channelAdminLogEventActionToggleAntiSpam(let newValue): return ("channelAdminLogEventActionToggleAntiSpam", [("newValue", newValue as Any)]) + case .channelAdminLogEventActionToggleAutotranslation(let newValue): + return ("channelAdminLogEventActionToggleAutotranslation", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleForum(let newValue): return ("channelAdminLogEventActionToggleForum", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleGroupCallSetting(let joinMuted): @@ -1677,6 +1686,19 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionToggleAutotranslation(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.Bool? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Bool + } + let _c1 = _1 != nil + if _c1 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionToggleAutotranslation(newValue: _1!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionToggleForum(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 804687918d..e3c0df82da 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -760,6 +760,42 @@ public extension Api.stories { } } +public extension Api.stories { + enum CanSendStoryCount: TypeConstructorDescription { + case canSendStoryCount(countRemains: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .canSendStoryCount(let countRemains): + if boxed { + buffer.appendInt32(-1014513586) + } + serializeInt32(countRemains, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .canSendStoryCount(let countRemains): + return ("canSendStoryCount", [("countRemains", countRemains as Any)]) + } + } + + public static func parse_canSendStoryCount(_ reader: BufferReader) -> CanSendStoryCount? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.stories.CanSendStoryCount.canSendStoryCount(countRemains: _1!) + } + else { + return nil + } + } + + } +} public extension Api.stories { enum FoundStories: TypeConstructorDescription { case foundStories(flags: Int32, count: Int32, stories: [Api.FoundStory], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) @@ -1560,55 +1596,3 @@ public extension Api.updates { } } -public extension Api.updates { - enum State: TypeConstructorDescription { - case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - if boxed { - buffer.appendInt32(-1519637954) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(qts, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(seq, buffer: buffer, boxed: false) - serializeInt32(unreadCount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) - } - } - - public static func parse_state(_ reader: BufferReader) -> State? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index e76c07649a..281a495dbf 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,55 @@ +public extension Api.updates { + enum State: TypeConstructorDescription { + case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + if boxed { + buffer.appendInt32(-1519637954) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(qts, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(seq, buffer: buffer, boxed: false) + serializeInt32(unreadCount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) + } + } + + public static func parse_state(_ reader: BufferReader) -> State? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) + } + else { + return nil + } + } + + } +} public extension Api.upload { enum CdnFile: TypeConstructorDescription { case cdnFile(bytes: Buffer) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 35b5eecc84..f937d610ca 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -3590,6 +3590,22 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func toggleAutotranslation(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(377471137) + channel.serialize(buffer, true) + enabled.serialize(buffer, true) + return (FunctionDescription(name: "channels.toggleAutotranslation", parameters: [("channel", String(describing: channel)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.channels { static func toggleForum(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -11139,15 +11155,15 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-941629475) + buffer.appendInt32(820732912) peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.canSendStory", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "stories.canSendStory", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.CanSendStoryCount? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.stories.CanSendStoryCount? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.stories.CanSendStoryCount } return result }) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index cfaf228f4f..d3954aecf4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1702,7 +1702,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor } public enum StoriesUploadAvailability { - case available + case available(remainingCount: Int32) case weeklyLimit case monthlyLimit case expiringLimit @@ -1729,10 +1729,9 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories. return account.network.request(Api.functions.stories.canSendStory(peer: inputPeer)) |> map { result -> StoriesUploadAvailability in - if result == .boolTrue { - return .available - } else { - return .unknownLimit + switch result { + case let .canSendStoryCount(countRemains): + return .available(remainingCount: countRemains) } } |> `catch` { error -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 9abcd3c23f..9330c8db39 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -179,6 +179,7 @@ public enum BotPaymentFormRequestError { case alreadyActive case noPaymentNeeded case disallowedStarGift + case starGiftResellTooEarly(Int32) } extension BotPaymentInvoice { @@ -482,6 +483,11 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw return .fail(.noPaymentNeeded) } else if error.errorDescription == "USER_DISALLOWED_STARGIFTS" { return .fail(.disallowedStarGift) + } else if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") { + let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...]) + if let value = Int32(timeout) { + return .fail(.starGiftResellTooEarly(value)) + } } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 73c0fffe67..138a3a7b22 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -847,8 +847,14 @@ public enum TransferStarGiftError { public enum BuyStarGiftError { case generic + case starGiftResellTooEarly(Int32) } +public enum UpdateStarGiftPriceError { + case generic +} + + public enum UpgradeStarGiftError { case generic } @@ -858,7 +864,12 @@ func _internal_buyStarGift(account: Account, slug: String, peerId: EnginePeer.Id return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil) |> map(Optional.init) |> `catch` { error -> Signal in - return .fail(.generic) + switch error { + case let .starGiftResellTooEarly(value): + return .fail(.starGiftResellTooEarly(value)) + default: + return .fail(.generic) + } } |> mapToSignal { paymentForm in if let paymentForm { @@ -1487,7 +1498,13 @@ private final class ProfileGiftsContextImpl { } let disposable = MetaDisposable() disposable.set( - _internal_upgradeStarGift(account: self.account, formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in + (_internal_upgradeStarGift( + account: self.account, + formId: formId, + reference: reference, + keepOriginalInfo: keepOriginalInfo + ) + |> deliverOn(self.queue)).startStrict(next: { [weak self] result in guard let self else { return } @@ -1509,39 +1526,54 @@ private final class ProfileGiftsContextImpl { } } - func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) { - self.actionDisposable.set( - _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price).startStrict() - ) - - - if let index = self.gifts.firstIndex(where: { gift in - if gift.reference == reference { - return true - } - return false - }) { - if case let .unique(uniqueGift) = self.gifts[index].gift { - let updatedUniqueGift = uniqueGift.withResellStars(price) - let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)) - self.gifts[index] = updatedGift + func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) -> Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable } + let disposable = MetaDisposable() + disposable.set( + (_internal_updateStarGiftResalePrice( + account: self.account, + reference: reference, + price: price + ) + |> deliverOn(self.queue)).startStrict(error: { error in + subscriber.putError(error) + }, completed: { + if let index = self.gifts.firstIndex(where: { gift in + if gift.reference == reference { + return true + } + return false + }) { + if case let .unique(uniqueGift) = self.gifts[index].gift { + let updatedUniqueGift = uniqueGift.withResellStars(price) + let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)) + self.gifts[index] = updatedGift + } + } + + if let index = self.filteredGifts.firstIndex(where: { gift in + if gift.reference == reference { + return true + } + return false + }) { + if case let .unique(uniqueGift) = self.filteredGifts[index].gift { + let updatedUniqueGift = uniqueGift.withResellStars(price) + let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)) + self.filteredGifts[index] = updatedGift + } + } + + self.pushState() + + subscriber.putCompletion() + }) + ) + return disposable } - - if let index = self.filteredGifts.firstIndex(where: { gift in - if gift.reference == reference { - return true - } - return false - }) { - if case let .unique(uniqueGift) = self.filteredGifts[index].gift { - let updatedUniqueGift = uniqueGift.withResellStars(price) - let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)) - self.filteredGifts[index] = updatedGift - } - } - - self.pushState() } func toggleStarGiftsNotifications(enabled: Bool) { @@ -1939,9 +1971,17 @@ public final class ProfileGiftsContext { } } - public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) { - self.impl.with { impl in - impl.updateStarGiftResellPrice(reference: reference, price: price) + public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.updateStarGiftResellPrice(reference: reference, price: price).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable } } @@ -2274,23 +2314,21 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer } } -func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal { +func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal { return account.postbox.transaction { transaction in return reference.apiStarGiftReference(transaction: transaction) } + |> castError(UpdateStarGiftPriceError.self) |> mapToSignal { starGift in guard let starGift else { return .complete() } return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) + |> mapError { error -> UpdateStarGiftPriceError in + return .generic } - |> mapToSignal { updates -> Signal in - if let updates { - account.stateManager.addUpdates(updates) - } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) return .complete() } |> ignoreValues @@ -2496,6 +2534,66 @@ private final class ResaleGiftsContextImpl { self.loadMore() } + + func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal { + return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId) + |> afterCompleted { [weak self] in + guard let self else { + return + } + self.queue.async { + if let count = self.count { + self.count = max(0, count - 1) + } + self.gifts.removeAll(where: { gift in + if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug { + return true + } + return false + }) + self.pushState() + } + } + } + + func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable + } + let disposable = MetaDisposable() + disposable.set( + (_internal_updateStarGiftResalePrice( + account: self.account, + reference: .slug(slug: slug), + price: price + ) + |> deliverOn(self.queue)).startStrict(error: { error in + subscriber.putError(error) + }, completed: { + if let index = self.gifts.firstIndex(where: { gift in + if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug { + return true + } + return false + }) { + if let price { + if case let .unique(uniqueGift) = self.gifts[index] { + self.gifts[index] = .unique(uniqueGift.withResellStars(price)) + } + } else { + self.gifts.remove(at: index) + } + } + + self.pushState() + + subscriber.putCompletion() + }) + ) + return disposable + } + } private func pushState() { let state = ResaleGiftsContext.State( @@ -2584,6 +2682,34 @@ public final class ResaleGiftsContext { impl.updateFilterAttributes(attributes) } } + + public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.buyStarGift(slug: slug, peerId: peerId).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } + + public func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.updateStarGiftResellPrice(slug: slug, price: price).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } public var currentState: ResaleGiftsContext.State? { var state: ResaleGiftsContext.State? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 87b914ce58..2fcf1f873a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -153,7 +153,7 @@ public extension TelegramEngine { return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled) } - public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal { + public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal { return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 39e07ac740..6937e7b94a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -94,6 +94,7 @@ public enum AdminLogEventAction { case changeStatus(prev: PeerEmojiStatus?, new: PeerEmojiStatus?) case changeEmojiPack(prev: StickerPackReference?, new: StickerPackReference?) case participantSubscriptionExtended(prev: RenderedChannelParticipant, new: RenderedChannelParticipant) + case toggleAutoTranslation(Bool) } public enum ChannelAdminLogEventError { @@ -457,6 +458,8 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] { action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) } + case let .channelAdminLogEventActionToggleAutotranslation(newValue): + action = .toggleAutoTranslation(boolFromApiValue(newValue)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) if let action = action { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index f2cf4d1cd2..3bd97fb244 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -3469,7 +3469,11 @@ public class CameraScreenImpl: ViewController, CameraScreen { } self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] availability in - guard let self, availability != .available else { + guard let self else { + return + } + if case let .available(remainingCount) = availability { + let _ = remainingCount return } self.node.postingAvailable = false diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 8b3855f064..30cff91d9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -2282,6 +2282,33 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + case let .toggleAutoTranslation(value): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + var text: String = "" + var entities: [MessageTextEntity] = [] + if value { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOn(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOff(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index b327c778ce..52bd22d3ee 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -370,18 +370,10 @@ final class GiftOptionsScreenComponent: Component { var isSoldOut = false switch gift { case let .generic(gift): - if let availability = gift.availability, availability.resale > 0 { - //TODO:localize - //TODO:unmock - ribbon = GiftItemComponent.Ribbon( - text: "resale", - color: .green - ) - } else if let _ = gift.soldOut { + if let _ = gift.soldOut { if let availability = gift.availability, availability.resale > 0 { - //TODO:localize ribbon = GiftItemComponent.Ribbon( - text: "resale", + text: environment.strings.Gift_Options_Gift_Resale, color: .green ) } else { @@ -415,7 +407,7 @@ final class GiftOptionsScreenComponent: Component { let subject: GiftItemComponent.Subject switch gift { case let .generic(gift): - if let availability = gift.availability, let minResaleStars = availability.minResaleStars { + if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars { subject = .starGift(gift: gift, price: "⭐️ \(minResaleStars)+") } else { subject = .starGift(gift: gift, price: "⭐️ \(gift.price)") @@ -450,7 +442,7 @@ final class GiftOptionsScreenComponent: Component { mainController = controller } if case let .generic(gift) = gift { - if let availability = gift.availability, availability.remains == 0 || (availability.resale > 0) { + if let availability = gift.availability, availability.remains == 0 { if availability.resale > 0 { let storeController = component.context.sharedContext.makeGiftStoreController( context: component.context, @@ -1296,7 +1288,7 @@ final class GiftOptionsScreenComponent: Component { starsAmountsSet.insert(gift.price) if let availability = gift.availability { hasLimited = true - if availability.resale > 0 { + if availability.remains == 0 && availability.resale > 0 { hasResale = true } } @@ -1317,10 +1309,9 @@ final class GiftOptionsScreenComponent: Component { )) if hasResale { - //TODO:localize tabSelectorItems.append(TabSelectorComponent.Item( id: AnyHashable(StarsFilter.resale.rawValue), - title: "Resale" + title: strings.Gift_Options_Gift_Filter_Resale )) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index cdc4322ae2..d4c2e9837a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -82,6 +82,7 @@ final class GiftSetupScreenComponent: Component { private let navigationTitle = ComponentView() private let remainingCount = ComponentView() + private let resaleSection = ComponentView() private let introContent = ComponentView() private let introSection = ComponentView() private let starsSection = ComponentView() @@ -787,6 +788,59 @@ final class GiftSetupScreenComponent: Component { contentHeight += sectionSpacing } + if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability, availability.resale > 0 { + let resaleSectionSize = self.resaleSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "Available for Resale", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)) + ) + )), + ], alignment: .left, spacing: 2.0)), + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator), + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0 + ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), + action: { [weak self] _ in + guard let self, let component = self.component, let controller = environment.controller() else { + return + } + let storeController = component.context.sharedContext.makeGiftStoreController( + context: component.context, + peerId: component.peerId, + gift: starGift + ) + controller.push(storeController) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize) + if let resaleSectionView = self.resaleSection.view { + if resaleSectionView.superview == nil { + self.scrollView.addSubview(resaleSectionView) + } + transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame) + } + contentHeight += resaleSectionSize.height + contentHeight += sectionSpacing + } + let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) var introSectionItems: [AnyComponentWithIdentity] = [] diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 6023e86c6f..0b2912e02c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -139,21 +139,10 @@ 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 { - 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 - } + return gifts } else { return nil } @@ -253,15 +242,14 @@ final class GiftStoreScreenComponent: Component { } let giftController = GiftViewScreen( context: component.context, - subject: .uniqueGift(uniqueGift, state.peerId) - ) - giftController.onBuySuccess = { [weak self] in - guard let self else { - return + subject: .uniqueGift(uniqueGift, state.peerId), + buyGift: { slug, peerId in + return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId) ?? .complete() + }, + updateResellStars: { price in + return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete() } - self.removedStarGifts.insert(uniqueGift.slug) - self.state?.updated(transition: .spring(duration: 0.3)) - } + ) mainController.push(giftController) } } @@ -507,15 +495,17 @@ final class GiftStoreScreenComponent: Component { //TODO:localize var items: [ContextMenuItem] = [] - items.append(.custom(SearchContextItem( - context: component.context, - placeholder: "Search", - value: "", - valueChanged: { value in - searchQueryPromise.set(value) - } - ), false)) - items.append(.separator) + if modelAttributes.count >= 8 { + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + } items.append(.custom(GiftAttributeListContextItem( context: component.context, attributes: modelAttributes, @@ -597,15 +587,17 @@ final class GiftStoreScreenComponent: Component { //TODO:localize var items: [ContextMenuItem] = [] - items.append(.custom(SearchContextItem( - context: component.context, - placeholder: "Search", - value: "", - valueChanged: { value in - searchQueryPromise.set(value) - } - ), false)) - items.append(.separator) + if backdropAttributes.count >= 8 { + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + } items.append(.custom(GiftAttributeListContextItem( context: component.context, attributes: backdropAttributes, @@ -687,15 +679,17 @@ final class GiftStoreScreenComponent: Component { //TODO:localize var items: [ContextMenuItem] = [] - items.append(.custom(SearchContextItem( - context: component.context, - placeholder: "Search", - value: "", - valueChanged: { value in - searchQueryPromise.set(value) - } - ), false)) - items.append(.separator) + if patternAttributes.count >= 8 { + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + } items.append(.custom(GiftAttributeListContextItem( context: component.context, attributes: patternAttributes, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 76bf4b098c..802039c050 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -460,8 +460,6 @@ private final class GiftViewSheetContent: CombinedComponent { guard let self, let controller = self.getController() as? GiftViewScreen else { return } - controller.onBuySuccess() - self.inProgress = false var animationFile: TelegramMediaFile? @@ -2902,7 +2900,6 @@ public class GiftViewScreen: ViewControllerComponentContainer { let updateSubject = ActionSlot() public var disposed: () -> Void = {} - public var onBuySuccess: () -> Void = {} fileprivate var showBalance = false { didSet { @@ -2922,7 +2919,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { transferGift: ((Bool, EnginePeer.Id) -> Signal)? = nil, upgradeGift: ((Int64?, Bool) -> Signal)? = nil, buyGift: ((String, EnginePeer.Id) -> Signal)? = nil, - updateResellStars: ((Int64?) -> Void)? = nil, + updateResellStars: ((Int64?) -> Signal)? = nil, togglePinnedToTop: ((Bool) -> Bool)? = nil, shareStory: ((StarGift.UniqueGift) -> Void)? = nil ) { @@ -3413,6 +3410,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))" + let reference = arguments.reference ?? .slug(slug: gift.slug) //TODO:localize if let resellStars = gift.resellStars, resellStars > 0, !update { @@ -3425,44 +3423,39 @@ public class GiftViewScreen: ViewControllerComponentContainer { guard let self else { return } - - 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 text = "\(giftTitle) is removed from sale." - let tooltipController = UndoOverlayController( - presentationData: presentationData, - content: .universalImage( - image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!, - size: nil, - title: nil, - text: text, - customUndoText: nil, - timeout: 3.0 - ), - position: .bottom, - animateInAsReplacement: false, - appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), - action: { action in - return false + let _ = ((updateResellStars?(nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil)) + |> deliverOnMainQueue).startStandalone(error: { error in + + }, completed: { + 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.present(tooltipController, in: .window(.root)) - - if let updateResellStars { - updateResellStars(nil) - } else { - let reference = arguments.reference ?? .slug(slug: gift.slug) - let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil) - |> deliverOnMainQueue).startStandalone() - } + + let text = "\(giftTitle) is removed from sale." + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!, + size: nil, + title: nil, + text: text, + customUndoText: nil, + timeout: 3.0 + ), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { action in + return false + } + ) + self.present(tooltipController, in: .window(.root)) + }) }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) @@ -3476,46 +3469,47 @@ public class GiftViewScreen: ViewControllerComponentContainer { return } - 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 \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars." - } - - let tooltipController = UndoOverlayController( - presentationData: presentationData, - content: .universalImage( - image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!, - size: nil, - title: nil, - text: text, - customUndoText: nil, - timeout: 3.0 - ), - position: .bottom, - animateInAsReplacement: false, - appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), - action: { action in - return false + let _ = ((updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price)) + |> deliverOnMainQueue).startStandalone(error: { error in + + }, completed: { [weak self] in + guard let self else { + return } - ) - self.present(tooltipController, in: .window(.root)) - - if let updateResellStars { - updateResellStars(price) - } else { - let reference = arguments.reference ?? .slug(slug: gift.slug) - let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price) - |> deliverOnMainQueue).startStandalone() - } + + 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 \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars." + } + + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!, + size: nil, + title: nil, + text: text, + customUndoText: nil, + timeout: 3.0 + ), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { action in + return false + } + ) + self.present(tooltipController, in: .window(.root)) + }) }) self.push(resellController) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 90f3536f54..95104c8142 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -352,6 +352,8 @@ public final class MediaEditor { return state.position } } + + public var maxDuration: Double = 60.0 public var duration: Double? { if let stickerEntity = self.stickerEntity { @@ -360,7 +362,7 @@ public final class MediaEditor { if let trimRange = self.values.videoTrimRange { return trimRange.upperBound - trimRange.lowerBound } else { - return min(60.0, self.playerPlaybackState.duration) + return min(self.maxDuration, self.playerPlaybackState.duration) } } else { return nil @@ -369,7 +371,7 @@ public final class MediaEditor { public var mainVideoDuration: Double? { if self.player != nil { - return min(60.0, self.playerPlaybackState.duration) + return min(self.maxDuration, self.playerPlaybackState.duration) } else { return nil } @@ -377,7 +379,7 @@ public final class MediaEditor { public var additionalVideoDuration: Double? { if let additionalPlayer = self.additionalPlayers.first { - return min(60.0, additionalPlayer.currentItem?.asset.duration.seconds ?? 0.0) + return min(self.maxDuration, additionalPlayer.currentItem?.asset.duration.seconds ?? 0.0) } else { return nil } @@ -385,7 +387,15 @@ public final class MediaEditor { public var originalDuration: Double? { if self.player != nil || !self.additionalPlayers.isEmpty { - return min(60.0, self.playerPlaybackState.duration) + return self.playerPlaybackState.duration + } else { + return nil + } + } + + public var originalCappedDuration: Double? { + if self.player != nil || !self.additionalPlayers.isEmpty { + return min(self.maxDuration, self.playerPlaybackState.duration) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index d0b1e891aa..47b4d4c792 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -909,7 +909,7 @@ public final class MediaEditorValues: Codable, Equatable { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } - func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { + public func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 4edda83f7c..7e4eac1d55 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -327,7 +327,7 @@ final class MediaEditorScreenComponent: Component { private let switchCameraButton = ComponentView() private let selectionButton = ComponentView() - private let selectionPanel = ComponentView() + private var selectionPanel: ComponentView? private let textCancelButton = ComponentView() private let textDoneButton = ComponentView() @@ -577,6 +577,11 @@ final class MediaEditorScreenComponent: Component { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) } + + if let view = self.selectionButton.view { + view.layer.animateAlpha(from: 0.0, to: view.alpha, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } } } @@ -589,14 +594,14 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: view, scale: 0.1) } - let buttons = [ + let toolbarButtons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] - for button in buttons { + for button in toolbarButtons { if let view = button.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false) @@ -617,19 +622,17 @@ final class MediaEditorScreenComponent: Component { } } - if let view = self.saveButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } + let topButtons = [ + self.saveButton, + self.muteButton, + self.playbackButton + ] - if let view = self.muteButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.playbackButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + for button in topButtons { + if let view = button.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } if let view = self.scrubber?.view { @@ -638,35 +641,30 @@ final class MediaEditorScreenComponent: Component { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } - if let view = self.undoButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } + let stickerButtons = [ + self.undoButton, + self.eraseButton, + self.restoreButton, + self.outlineButton, + self.cutoutButton + ] - if let view = self.eraseButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.restoreButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.outlineButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.cutoutButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + for button in stickerButtons { + if let view = button.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } if let view = self.textSize.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } + + if let view = self.selectionButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } func animateOutToTool(inPlace: Bool, transition: ComponentTransition) { @@ -2000,135 +1998,6 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } - - if controller.node.items.count > 1 { - let selectionButtonSize = self.selectionButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent( - SelectionPanelButtonContentComponent( - count: Int32(controller.node.items.count(where: { $0.isEnabled })), - isSelected: self.isSelectionPanelOpen, - tag: nil - ) - ), - effectAlignment: .center, - 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 - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 33.0) - ) - let selectionButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0), - size: selectionButtonSize - ) - if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View { - if selectionButtonView.superview == nil { - self.addSubview(selectionButtonView) - } - transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) - transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) - 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( - origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0), - size: CGSize(width: availableSize.width - 24.0, height: 120.0) - ) - - var selectedItemId = "" - if case let .asset(asset) = controller.node.subject { - selectedItemId = asset.localIdentifier - } - - let _ = self.selectionPanel.update( - transition: transition, - component: AnyComponent( - SelectionPanelComponent( - previewContainerView: controller.node.previewContentContainerView, - frame: selectionPanelFrame, - items: controller.node.items, - selectedItemId: selectedItemId, - itemTapped: { [weak self, weak controller] id in - guard let self, let controller else { - return - } - self.isSelectionPanelOpen = false - 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 - guard let self, let controller else { - return - } - if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) { - controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled - } - self.state?.updated(transition: .spring(duration: 0.3)) - }, - itemReordered: { [weak self, weak controller] fromId, toId in - guard let self, let controller else { - return - } - guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else { - return - } - let fromItem = controller.node.items[fromIndex] - let toItem = controller.node.items[toIndex] - controller.node.items[fromIndex] = toItem - controller.node.items[toIndex] = fromItem - self.state?.updated(transition: .spring(duration: 0.3)) - - controller.hapticFeedback.tap() - } - ) - ), - environment: {}, - containerSize: availableSize - ) - if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { - if selectionPanelView.superview == nil { - self.insertSubview(selectionPanelView, belowSubview: selectionButtonView) - if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { - selectionPanelView.animateIn(from: buttonView) - } - } - selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) - } - } else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { - if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { - selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in - selectionPanelView?.removeFromSuperview() - }) - } else { - selectionPanelView.removeFromSuperview() - } - } - } - } } else { inputPanelSize = CGSize(width: 0.0, height: 12.0) } @@ -2136,20 +2005,24 @@ final class MediaEditorScreenComponent: Component { if case .stickerEditor = controller.mode { } else { + var selectionButtonInset: CGFloat = 0.0 + if let playerState = state.playerState { let scrubberInset: CGFloat = 9.0 let minDuration: Double let maxDuration: Double + var segmentDuration: Double? if playerState.isAudioOnly { minDuration = 5.0 maxDuration = 15.0 } else { minDuration = 1.0 if case .avatarEditor = controller.mode { - maxDuration = 10.0 + maxDuration = 9.9 } else { - maxDuration = storyMaxVideoDuration + maxDuration = storyMaxCombinedVideoDuration + segmentDuration = storyMaxVideoDuration } } @@ -2224,6 +2097,7 @@ final class MediaEditorScreenComponent: Component { position: playerState.position, minDuration: minDuration, maxDuration: maxDuration, + segmentDuration: segmentDuration, isPlaying: playerState.isPlaying, tracks: visibleTracks, isCollage: isCollage, @@ -2363,6 +2237,7 @@ final class MediaEditorScreenComponent: Component { } let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0 - scrubberBottomOffset), size: scrubberSize) + selectionButtonInset = scrubberSize.height + 11.0 if let scrubberView = scrubber.view { var animateIn = false if scrubberView.superview == nil { @@ -2407,6 +2282,146 @@ final class MediaEditorScreenComponent: Component { } } } + + if controller.node.items.count > 1 { + let selectionButtonSize = self.selectionButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent( + SelectionPanelButtonContentComponent( + count: Int32(controller.node.items.count(where: { $0.isEnabled })), + isSelected: self.isSelectionPanelOpen, + tag: nil + ) + ), + effectAlignment: .center, + 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(transition: .spring(duration: 0.3)) + + controller.hapticFeedback.impact(.light) + } + }, + animateAlpha: false + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + let selectionButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: availableSize.height - environment.safeInsets.bottom - selectionButtonSize.height + controlsBottomInset - inputPanelSize.height - 3.0 - selectionButtonInset), + size: selectionButtonSize + ) + if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View { + if selectionButtonView.superview == nil { + self.addSubview(selectionButtonView) + } + transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) + transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) + 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( + origin: CGPoint(x: 12.0, y: selectionButtonFrame.minY - 130.0), + size: CGSize(width: availableSize.width - 24.0, height: 120.0) + ) + + var selectedItemId = "" + if case let .asset(asset) = controller.node.subject { + selectedItemId = asset.localIdentifier + } + + let selectionPanel: ComponentView + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + let _ = selectionPanel.update( + transition: transition, + component: AnyComponent( + SelectionPanelComponent( + previewContainerView: controller.node.previewContentContainerView, + frame: selectionPanelFrame, + items: controller.node.items, + selectedItemId: selectedItemId, + itemTapped: { [weak self, weak controller] id in + guard let self, let controller else { + return + } + self.isSelectionPanelOpen = false + 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 + guard let self, let controller else { + return + } + if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) { + controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled + } + self.state?.updated(transition: .spring(duration: 0.3)) + }, + itemReordered: { [weak self, weak controller] fromId, toId in + guard let self, let controller else { + return + } + guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else { + return + } + let fromItem = controller.node.items[fromIndex] + let toItem = controller.node.items[toIndex] + controller.node.items[fromIndex] = toItem + controller.node.items[toIndex] = fromItem + self.state?.updated(transition: .spring(duration: 0.3)) + + controller.hapticFeedback.tap() + } + ) + ), + environment: {}, + containerSize: availableSize + ) + if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View { + if selectionPanelView.superview == nil { + self.insertSubview(selectionPanelView, belowSubview: selectionButtonView) + if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateIn(from: buttonView) + } + } + selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) + } + } else if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View { + if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in + selectionPanelView?.removeFromSuperview() + }) + } else { + selectionPanelView.removeFromSuperview() + } + } + } + } + } } if case .stickerEditor = controller.mode { @@ -2821,6 +2836,8 @@ final class MediaEditorScreenComponent: Component { let storyDimensions = CGSize(width: 1080.0, height: 1920.0) let storyMaxVideoDuration: Double = 60.0 +let storyMaxCombinedVideoCount: Int = 3 +let storyMaxCombinedVideoDuration: Double = storyMaxVideoDuration * Double(storyMaxCombinedVideoCount) public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UIDropInteractionDelegate { public enum Mode { @@ -3489,6 +3506,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID values: initialValues, hasHistogram: true ) + mediaEditor.maxDuration = storyMaxCombinedVideoDuration if case .avatarEditor = controller.mode { mediaEditor.setVideoIsMuted(true) } else if case let .coverEditor(dimensions) = controller.mode { @@ -5075,7 +5093,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID var audioTrimRange: Range? var audioOffset: Double? - if let videoDuration = mediaEditor.originalDuration { + if let videoDuration = mediaEditor.originalCappedDuration { if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound { audioOffset = -videoStart } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound { @@ -6694,7 +6712,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] availability in - guard let self, availability != .available else { + guard let self else { + return + } + if case .available = availability { return } @@ -7341,36 +7362,21 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return true } - 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 { + private func processMultipleItems(items: [EditingItem]) { + guard !items.isEmpty else { return } - 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] + var items = items + if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) { + var updatedCurrentItem = items[currentItemIndex] updatedCurrentItem.caption = self.node.getCaption() updatedCurrentItem.values = mediaEditor.values - self.node.items[currentItemIndex] = updatedCurrentItem + items[currentItemIndex] = updatedCurrentItem } let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: []) - let totalItems = self.node.items.count + let totalItems = items.count let dispatchGroup = DispatchGroup() @@ -7387,7 +7393,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } var order: [Int64] = [] - for (index, item) in self.node.items.enumerated() { + for (index, item) in items.enumerated() { guard item.isEnabled else { continue } @@ -7431,7 +7437,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID orderedResults.append(item) } } - self.completeWithMultipleResults(results: orderedResults) + self.completion(results, { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) } } } @@ -7452,13 +7465,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID 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)) @@ -7476,7 +7486,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } - // Calculate duration let duration: Double if let videoTrimRange = item.values?.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound @@ -7484,7 +7493,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID 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 @@ -7541,14 +7549,11 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID 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] = [] @@ -7557,13 +7562,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID 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 @@ -7664,10 +7666,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID 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) @@ -7680,6 +7678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID randomId = Int64.random(in: .min ... .max) } + let codableEntities = mediaEditor.values.entities var mediaAreas: [MediaArea] = [] if case let .draft(draft, _) = actualSubject { if draft.values.entities != codableEntities { @@ -8108,6 +8107,15 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }) } } + + private func updateMediaEditorEntities() { + guard let mediaEditor = self.node.mediaEditor 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) + } private var didComplete = false func requestStoryCompletion(animated: Bool) { @@ -8117,7 +8125,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.didComplete = true - self.dismissAllTooltips() + self.updateMediaEditorEntities() mediaEditor.stop() mediaEditor.invalidate() @@ -8127,11 +8135,42 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - if self.node.items.count(where: { $0.isEnabled }) > 1 { - self.processMultipleItems() + var multipleItems: [EditingItem] = [] + if self.node.items.count > 1 { + multipleItems = self.node.items.filter({ $0.isEnabled }) + } else if case let .asset(asset) = self.node.subject { + let duration: Double + if let playerDuration = mediaEditor.duration { + duration = playerDuration + } else { + duration = asset.duration + } + if duration > storyMaxVideoDuration { + let originalDuration = mediaEditor.originalDuration ?? asset.duration + let values = mediaEditor.values + + let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration))) + var start = values.videoTrimRange?.lowerBound ?? 0 + for _ in 0 ..< storyCount { + let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration)) + + var editingItem = EditingItem(asset: asset) + editingItem.caption = self.node.getCaption() + editingItem.values = trimmedValues + multipleItems.append(editingItem) + + start += storyMaxVideoDuration + } + } + } + + if multipleItems.count > 1 { + self.processMultipleItems(items: multipleItems) } else { self.processSingleItem() } + + self.dismissAllTooltips() } func requestStickerCompletion(animated: Bool) { @@ -8157,10 +8196,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - 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) - + self.updateMediaEditorEntities() + if let image = mediaEditor.resultImage { let values = mediaEditor.values.withUpdatedQualityPreset(.sticker) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: CGSize(width: 512, height: 512), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in @@ -8181,11 +8218,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - - 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) - + + self.updateMediaEditorEntities() + if let image = mediaEditor.resultImage { let values = mediaEditor.values.withUpdatedCoverDimensions(dimensions) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: dimensions.aspectFitted(CGSize(width: 1080, height: 1080)), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in @@ -8786,12 +8821,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } - let context = self.context + self.updateMediaEditorEntities() - 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) - let isSticker = toStickerResource != nil if !isSticker { self.previousSavedValues = mediaEditor.values @@ -8820,6 +8851,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }) } + let context = self.context if mediaEditor.resultIsVideo { if !isSticker { mediaEditor.maybePauseVideo() diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index 3a5b90a3b3..f7847fee37 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -84,6 +84,7 @@ public final class MediaScrubberComponent: Component { let position: Double let minDuration: Double let maxDuration: Double + let segmentDuration: Double? let isPlaying: Bool let tracks: [Track] @@ -112,6 +113,7 @@ public final class MediaScrubberComponent: Component { position: Double, minDuration: Double, maxDuration: Double, + segmentDuration: Double? = nil, isPlaying: Bool, tracks: [Track], isCollage: Bool, @@ -135,6 +137,7 @@ public final class MediaScrubberComponent: Component { self.position = position self.minDuration = minDuration self.maxDuration = maxDuration + self.segmentDuration = segmentDuration self.isPlaying = isPlaying self.tracks = tracks self.isCollage = isCollage @@ -171,6 +174,9 @@ public final class MediaScrubberComponent: Component { if lhs.maxDuration != rhs.maxDuration { return false } + if lhs.segmentDuration != rhs.segmentDuration { + return false + } if lhs.isPlaying != rhs.isPlaying { return false } @@ -624,6 +630,7 @@ public final class MediaScrubberComponent: Component { isSelected: isSelected, availableSize: availableSize, duration: self.duration, + segmentDuration: lowestVideoId == track.id ? component.segmentDuration : nil, transition: trackTransition ) trackLayout[id] = (CGRect(origin: CGPoint(x: 0.0, y: totalHeight), size: trackSize), trackTransition, animateTrackIn) @@ -675,6 +682,7 @@ public final class MediaScrubberComponent: Component { isSelected: false, availableSize: availableSize, duration: self.duration, + segmentDuration: nil, transition: trackTransition ) trackTransition.setFrame(view: trackView, frame: CGRect(origin: .zero, size: trackSize)) @@ -955,6 +963,9 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega fileprivate let audioContentMaskView: UIImageView fileprivate let audioIconView: UIImageView fileprivate let audioTitle = ComponentView() + + fileprivate var segmentTitles: [Int32: ComponentView] = [:] + fileprivate var segmentLayers: [Int32: SimpleLayer] = [:] fileprivate let videoTransparentFramesContainer = UIView() fileprivate var videoTransparentFrameLayers: [VideoFrameLayer] = [] @@ -1142,6 +1153,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega isSelected: Bool, availableSize: CGSize, duration: Double, + segmentDuration: Double?, transition: ComponentTransition ) -> CGSize { let previousParams = self.params @@ -1477,6 +1489,86 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega transition.setFrame(view: self.vibrancyView, frame: CGRect(origin: .zero, size: containerFrame.size)) transition.setFrame(view: self.vibrancyContainer, frame: CGRect(origin: .zero, size: containerFrame.size)) + var segmentCount = 0 + var segmentOrigin: CGFloat = 0.0 + var segmentWidth: CGFloat = 0.0 + if let segmentDuration { + if duration > segmentDuration { + let fraction = segmentDuration / duration + segmentCount = Int(ceil(duration / segmentDuration)) - 1 + segmentWidth = floorToScreenPixels(containerFrame.width * fraction) + } + if let trimRange = track.trimRange { + if trimRange.lowerBound > 0.0 { + let fraction = trimRange.lowerBound / duration + segmentOrigin = floorToScreenPixels(containerFrame.width * fraction) + } + let actualSegmentCount = Int(ceil((trimRange.upperBound - trimRange.lowerBound) / segmentDuration)) - 1 + segmentCount = min(actualSegmentCount, segmentCount) + } + } + + var validIds = Set() + var segmentFrame = CGRect(x: segmentOrigin + segmentWidth, y: 0.0, width: 1.0, height: containerFrame.size.height) + for i in 0 ..< segmentCount { + let id = Int32(i) + validIds.insert(id) + + let segmentLayer: SimpleLayer + let segmentTitle: ComponentView + + var segmentTransition = transition + if let currentLayer = self.segmentLayers[id], let currentTitle = self.segmentTitles[id] { + segmentLayer = currentLayer + segmentTitle = currentTitle + } else { + segmentTransition = .immediate + segmentLayer = SimpleLayer() + segmentLayer.backgroundColor = UIColor.white.cgColor + segmentTitle = ComponentView() + + self.segmentLayers[id] = segmentLayer + self.segmentTitles[id] = segmentTitle + + self.containerView.layer.addSublayer(segmentLayer) + } + + transition.setFrame(layer: segmentLayer, frame: segmentFrame) + + let segmentTitleSize = segmentTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "#\(i + 2)", font: Font.semibold(11.0), textColor: .white)), + textShadowColor: UIColor(rgb: 0x000000, alpha: 0.4), + textShadowBlur: 1.0 + )), + environment: {}, + containerSize: containerFrame.size + ) + if let view = segmentTitle.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + segmentTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: segmentFrame.maxX + 2.0, y: 2.0), size: segmentTitleSize)) + } + segmentFrame.origin.x += segmentWidth + } + + var removeIds: [Int32] = [] + for (id, segmentLayer) in self.segmentLayers { + if !validIds.contains(id) { + removeIds.append(id) + segmentLayer.removeFromSuperlayer() + if let segmentTitle = self.segmentTitles[id] { + segmentTitle.view?.removeFromSuperview() + } + } + } + for id in removeIds { + self.segmentLayers.removeValue(forKey: id) + self.segmentTitles.removeValue(forKey: id) + } + return scrubberSize } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index b3a328a3b1..fc4b92105e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -613,6 +613,8 @@ private final class PeerInfoInteraction { let openBirthdayContextMenu: (ASDisplayNode, ContextGesture?) -> Void let editingOpenAffiliateProgram: () -> Void let editingOpenVerifyAccounts: () -> Void + let editingToggleAutoTranslate: (Bool) -> Void + let displayAutoTranslateLocked: () -> Void let getController: () -> ViewController? init( @@ -683,6 +685,8 @@ private final class PeerInfoInteraction { openBirthdayContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, editingOpenAffiliateProgram: @escaping () -> Void, editingOpenVerifyAccounts: @escaping () -> Void, + editingToggleAutoTranslate: @escaping (Bool) -> Void, + displayAutoTranslateLocked: @escaping () -> Void, getController: @escaping () -> ViewController? ) { self.openUsername = openUsername @@ -752,6 +756,8 @@ private final class PeerInfoInteraction { self.openBirthdayContextMenu = openBirthdayContextMenu self.editingOpenAffiliateProgram = editingOpenAffiliateProgram self.editingOpenVerifyAccounts = editingOpenVerifyAccounts + self.editingToggleAutoTranslate = editingToggleAutoTranslate + self.displayAutoTranslateLocked = displayAutoTranslateLocked self.getController = getController } } @@ -2154,6 +2160,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemBanned = 11 let ItemRecentActions = 12 let ItemAffiliatePrograms = 13 + let ItemPeerAutoTranslate = 14 let isCreator = channel.flags.contains(.isCreator) @@ -2268,6 +2275,18 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { interaction.editingOpenNameColorSetup() })) + + var isLocked = true + if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel >= 3 { + isLocked = false + } + items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemPeerAutoTranslate, text: presentationData.strings.Channel_Info_AutoTranslate, value: false, icon: UIImage(bundleImageName: "Settings/Menu/AutoTranslate"), isLocked: isLocked, toggled: { value in + if isLocked { + interaction.displayAutoTranslateLocked() + } else { + interaction.editingToggleAutoTranslate(value) + } + })) } var canEditMembers = false @@ -3194,6 +3213,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } self.editingOpenVerifyAccounts() + }, editingToggleAutoTranslate: { [weak self] isEnabled in + guard let self else { + return + } + self.toggleAutoTranslate(isEnabled: isEnabled) + }, displayAutoTranslateLocked: { [weak self] in + guard let self else { + return + } + self.displayAutoTranslateLocked() }, getController: { [weak self] in return self?.controller @@ -9127,6 +9156,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } + private func toggleAutoTranslate(isEnabled: Bool) { + + } + + private func displayAutoTranslateLocked() { + let _ = combineLatest( + queue: Queue.mainQueue(), + context.engine.peers.getChannelBoostStatus(peerId: self.peerId), + context.engine.peers.getMyBoostStatus() + ).startStandalone(next: { [weak self] boostStatus, myBoostStatus in + guard let self, let controller = self.controller, let boostStatus, let myBoostStatus else { + return + } + let boostController = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: self.peerId, subject: .autoTranslate, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in + if let self { + self.openStats(section: .boosts, boostStatus: boostStatus) + } + }) + controller.push(boostController) + }) + } + private func toggleForumTopics(isEnabled: Bool) { guard let data = self.data, let peer = data.peer else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 1b70f7765d..2c7db7f507 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -608,9 +608,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr }, updateResellStars: { [weak self] price in guard let self, let reference = product.reference else { - return + return .never() } - self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price) + return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price) }, togglePinnedToTop: { [weak self] pinnedToTop in guard let self else { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 8b4021c46c..7858518c4d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -173,6 +173,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) + let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let controller = environment.controller @@ -366,8 +368,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { } case let .transaction(transaction, parentPeer): if let starGift = transaction.starGift { - titleText = strings.Stars_Transaction_Gift_Title - descriptionText = "" + switch starGift { + case .generic: + titleText = strings.Stars_Transaction_Gift_Title + descriptionText = "" + case let .unique(gift): + titleText = gift.title + descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" + } count = transaction.count transactionId = transaction.id date = transaction.date @@ -665,14 +673,23 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } else { amountText = "+ \(formattedAmount)" - countColor = theme.list.itemDisclosureActions.constructive.fillColor + if case .unique = giftAnimationSubject { + countColor = .white + } else { + countColor = theme.list.itemDisclosureActions.constructive.fillColor + } } - + + var titleFont = Font.bold(25.0) + if case .unique = giftAnimationSubject { + titleFont = Font.bold(20.0) + } + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: titleText, - font: Font.bold(25.0), + font: titleFont, textColor: headerTextColor, paragraphAlignment: .center )), @@ -723,7 +740,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { if let giftAnimationSubject { let animationHeight: CGFloat if case .unique = giftAnimationSubject { - animationHeight = 240.0 + animationHeight = 268.0 } else { animationHeight = 210.0 } @@ -731,7 +748,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { component: GiftCompositionComponent( context: component.context, theme: theme, - subject: giftAnimationSubject + subject: giftAnimationSubject, + externalState: giftCompositionExternalState ), availableSize: CGSize(width: context.availableSize.width, height: animationHeight), transition: .immediate @@ -816,6 +834,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_GiftUpgrade, font: tableFont, textColor: tableTextColor))) ) )) + } else if case .unique = giftAnimationSubject { + tableItems.append(.init( + id: "reason", + title: strings.Stars_Transaction_Giveaway_Reason, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: count < StarsAmount.zero ? strings.Stars_Transaction_GiftPurchase : strings.Stars_Transaction_GiftSale, font: tableFont, textColor: tableTextColor))) + ) + )) } if isGift, toPeer == nil { @@ -1300,13 +1326,29 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) var originY: CGFloat = 156.0 - if let _ = giftAnimationSubject { - originY += 18.0 + switch giftAnimationSubject { + case .generic: + originY += 20.0 + case .unique: + originY += 34.0 + default: + break } context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) ) - originY += 21.0 + if case .unique = giftAnimationSubject { + originY += 17.0 + } else { + originY += 21.0 + } + + let vibrantColor: UIColor + if let previewPatternColor = giftCompositionExternalState.previewPatternColor { + vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) + } else { + vibrantColor = UIColor.white.withAlphaComponent(0.6) + } var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { @@ -1316,8 +1358,18 @@ private final class StarsTransactionSheetContent: CombinedComponent { if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } + + var textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + var textColor = theme.actionSheet.secondaryTextColor + if case .unique = giftAnimationSubject { + textFont = Font.regular(13.0) + textColor = vibrantColor + } else if countOnTop && !isSubscriber { + textColor = theme.list.itemPrimaryTextColor + } + let linkColor = theme.actionSheet.controlAccentColor - let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -1362,7 +1414,13 @@ private final class StarsTransactionSheetContent: CombinedComponent { context.add(description .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0)) ) - originY += description.size.height + 10.0 + originY += description.size.height + + if case .unique = giftAnimationSubject { + originY += 6.0 + } else { + originY += 10.0 + } } let amountSpacing: CGFloat = countBackgroundColor != nil ? 4.0 : 1.0 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index f9aacdbe72..9d29afc635 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -317,9 +317,18 @@ final class StarsTransactionsListPanelComponent: Component { uniqueGift = gift } else { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) - itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift - if case let .generic(gift) = starGift { + switch starGift { + case let .generic(gift): itemFile = gift.file + itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + case let .unique(gift): + for attribute in gift.attributes { + if case let .model(_, file, _) = attribute { + itemFile = file + break + } + } + itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_GiftSale : environment.strings.Stars_Intro_Transaction_GiftPurchase } } } else if let _ = item.giveawayMessageId { diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json new file mode 100644 index 0000000000..3bc8fbe27a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "translation.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1715735f6382d527390329fc7817f3296770aafa GIT binary patch literal 12400 zcmeHNc{tQv*d|*|q6LZ6MA^#B{KhP%)xPhcA!QvIVJtHxO9-VzMfROUmPC|Ys1V6k z*_V(|vhU@aK{KlN{l4qFu5bCr`D5nHeYWR$&iTze=brnJrmVae06~L502l@)m|B6r z;N!=^P$_F7&cW7%hyz1qahLFBIBf-Kauc~g9@Ysv z1L;h*Y~T6f+zu_z9dfdahLX&3d+mr=^X)sdI+&T5bPTVtg5F3-+~O6@WY|v}R(qdz zCbO$Fwb0#dII4XxW-)%5?yfC^EX*Nk^Q#17BMoO+a}K^)#x2!e82h|E_?~Viw~EVN zlZ4CM*@b=`;g#DK*@!wXd1ttG>K89-E*5h*EwMtfqjr~fVEg+v$4Z8A@$%G4=kd`U z$NW6T zH{ZeNUARN=g7TaqpB5^VPv2Dkq=W^VT2s%LoZL2vEBV8=ZAf_KhK zayy8>%<2Wx7BG{qBv%XFvunpS>1I5B#O>WnkBnnOc!u)eCVm1)1=qZ{aNuf$f^{VCd0w;@r!gTrtvP=!7<8K2wq0 zV=>3?%COHPX-vh5he`9`eULKCD@j1>hz?j3#Cc<9JSu)dD=EcFhaq*_d#?hozyv|9 z{S8FkL^d>0BQCO`^UJX>JEk}m4qw04@FL743TCa!D7Y`k zr@G?4sTH6WRue{F%l(;;%TKqA$As_6)fPo-{!1sGZFkw+?)&ZKjEO>4lf+*2Z5OUP zmN9*jkN_a;m$zRAU8etdhquD*MbKEA6K@gwNf7kqV2{eU&^X`R)g{LzRw++oNjrYV zolf4l-u%+r;{EOXVDe!HM0fi8LSAZ`^4lNIy4@mKr0S~ZD#L$|s1)#E;2=GI>!aJm z2x2fodO=Z-KQd6i-!j{~DAF~mL#agLs!Y5ltH|hm;ag$Zk=hFRvR72+!3x1o@0!(& z_oHfbtB^I*HN}BKRlHR$Rhc11LPnzQdOt&!9&C~-)AUSwq1Ez0Gu}?!MA5|HUaDmV zcS=|SHpNMMJmOQ{PW5Wd>a#bK@23$}hXNagIpy9bW~UeqqpY?~!%4TVC+%ry$&odn z|0ZMrwNchrYSqK2V$SAE&lhFuFeRC$l~bLTz9ktdT9$#+Rzn-|aOUwS*utQTt8r?%8)+RsMw8OX*jpH{}B4 z{9YYaajOxoDGoUiVwCYUJut&6{dz`l`i~69^!D`SY#_I{O{b;foUDHTQ_IPO4|16{ zZ>{H_-LU!Is`V|T?c0-)>_yY>#?WlSlUTd@%f9w0L~hf@FsWMU&$2QwaKTWXeMf3%OukqPSw*LM!4Hs&Pf_#)ThiDKvC;G#25p)RLp8m4c~KAH`m z$eld*y?yA$_|~!Z5%kLs=8mnTuOjX&%lH9tIdQ5mEtEh(hhUAF(gf2ikpHKC| zy>R6?F;DPazC7rd)?F=4=laZ=tQxP7Ge_LHtoe!#fGUweJ0gV0A`Y-X+MLY$LGRsbuaZ<8$vuZzXyaO^8$Umr$pJ<8Io+=e;@kA9rMsLV zB-B~pY?zy4#!%b2i?NxL`m=q`Q_iCckGCD=eP+wu`XwvNYFCcPrPCiRir?l(Rz91v z#bgENA@L&wj2`9{hVa~naIxlnZD-)Ig3ik>ZZU>2!UW43lK}$98jdj?bGanN$+B>|h|bZ-1hNz!73 z%9lg)hX}Kot?n7k&!(#AKhamy<68r>KX@&$%jL|%)*{k1xeWaD2Hu2*?SNzY4 z(Hp!0mit5`CWcaaz7&ggr+&7o7;ZFcF$=GZdGGyxYN=`|>#<((v$&ZW`(FE@n?1Wa zNo*vN5K^7%eZ_R(!p+kS$+pY=)2A;oB*v#|C1v#>B*ZX3J`IM&vbvJ~{BX%4<%-Ruj*7Hj4l+CUd(?9NW7OcYhel#oPjo$QI&Xh|-c^ewOW?WLOnJf9 zrf7V0(C$8Hz@>~Ko)QlGA=}xB(XJZ$(9HAW`3{dN69>z6L({%}jCvehSXb2LQ1MfK zQmrsjt+e&Whvtjj?XESwdLuTK?KWar4Ax z$(l=aI{RJy4uB%uYhAK2Qa!Ou?q@D@=bN9uSos`lg`}&+(`mZE8OI#~Jv-qag&5ei zd3)C#M!wca)few}ZtlDHW9yYoSzuko-J6Hpd7G9?CQC=o#&#EFg~f`+jEEy`G8DJR zf*(HhF)dN7x<`7c6myL4UV7zioqJfY5|AvQ-pJ;0J5LVfs3K+lQot;xwyp$Vq&u_e zMQB26Yj|pFE41o}W(6Hl`6rYA<^}-}&enV_BP7=-StkfiO+Q!C$6v5G6+R1wD#^%5 zn>gak!IWH88+?v3&B|Z|wHGCK{r3b_=ZYOUp~Y<H(xyzbCCo%9Lx6f&+nMw~_|0 z4p{lT+P0Dcujaw0!B8Cs6I(|+69=5F*_Geg$rO|eP9eV?xp(ZBW|!Wg2Iu(T%C$>;S6IBe;|-+KuGrmNp66?W zVO`F|*+m!g4a@OAY#sNue5&WRmnZ3MiC)r2lo~rU6@9JWPPuWa0y%h>Zo8=o7nJzjnEzS*&FOJXl zu_x8-D;=3&Khb(?TP0K7iPT2YRDahGLvsz~AxFk}fe4;!?M00_A7fNWVY1ACO+W*- zIm}J0VCqRnqb=#vVY4vN5NXu(W~D&>gwq~^egnw`5;q=%k!JGdl=#gQ_VJSB1$&Q! z&~-Bw6WQ40-23-Bt*-EuLJs<=k>uIyAF{Nh$)`*61mj}T;Hqc1y_JohvNl@i8wl;T z{nX1FBxlDJ`T5{^=Wy=F_gJ&Q%2G#~3q;*K*if4$jibLQF?0h`YMbK5xQ0z{AcoId ze5FGTd;jPD*h}BJZvjVcz0d7IYaO%o41v}rSB_?%lEO{%N#iEdV&KCj6=K| zS=!olK$k6ZUKe5S2BCSXg`@`FGbL~Yt0T8WI~(wBAu%4{iEA3U*38u_Ec{mIXql&U z_)TsOO=xaD>rQzgg&NTH$f)x^^Sjy}IzASW=Ev~uB-TPagq+_g=V)Ciet|c7M&l&bQw>Zs&caq@He9eoGUDS?|hcxOp|W!bk!X0 z=J`HC|HD>!_pbcL!h^GZ&$B)Nhto51to$*7#wCMNF&R$D-V1>Osp*c1^0Am{;a2cH zsPN>3t5fgRxTZ_cdZlYYI$J!qez3a5*vveCR!QQ~@dlspNDao_IS;vCs_?pOd-(}; z;$uofv&VvVT&<;$9quWcp?NFY{=)|p5$SygYa)f-h!ZTlF1=*VD3gk#KV-Ey93s?e zsjmy`GA@Axy?!~x#5}H=%<7raZau}6bW5jWbn;l$t)Qri2uU_y-8zq&k?X#jbn67W zg|j_k80e>vw;dGY=hJe8@g4PErFM1LH>oYy*X4`y+1)MP8i7f^Q5S9n^4)|jH6V5% zx#kbO8i{Jz`BCK9ZMUOpqKX|+@964mi^D?0kMH`D?LaaBo?a*h+!e2j3&2H1eV89+ z>`0P6qRaT;(~zv>L#Kg9%D#K#y_zJWF*29pCap%a%r!mIGW^>HOPQAZCZG7?6b zcrsyU&fY6{P~d_S1X}Rca4j#<-S>}NYfsujYjV9DX|WZlvNnxI^^8;K%0jZt8ICIuLfsE zem$hhXYM$w&S&L=!h97S8uyc}`$4s5&AXX`fKDHK2)g`xgBp{Vc% zv$DagY%nVu%*qC{vcaruFe@9($_BHt!L0nRGb?Cn`uQ8PB0+`MGb@x_bv?6!q4uKW zuKx*t0>CIE*Z7M6X99)d+W#1VVh;lTqFuzOw9DUZ)Fv|<5Vihy#nH@`)(l8M&=^Vq zuNwo0L(p)TIEuV~zYKsQASekK5=kb@AZQdEgG7MgC`PFdPGci=$x(BzY(jA`V~>2njHaM?yp3 zXbg%xF%1F$2z-@+qG9A^sn*j_YY@$R8cMT&9Y%Fvy*=wZpc+ndVO=ZLhxLJ2H#;?A z>o96yX&B81GtfG8&0hd)y$=0{z)-_S^IJF`K8fM*2u5B)5UuY4d z9RjNPRETCh6(Vm7b+f3THHd0E6{DI?MW_d4y&b;{rbQXL6nE)Jgwd=5JGe zSqzXJrw*j~W(~3gS%7RoTA+Wpd#`-|tX{q`0E&9|1~z038?uHCS;K~`VMEriA#2!> zHEhTlHe?MOvWEXdSp)Ddx(481gbjZx8-U-X4QtYc6{P?c08<(%7u~gMx11AE;WUvV z+NE69<$#s9(`qJ;7guVlw|C0N)&4Xs!#~!b3Si3Zf9+2Hk6maWl&pr3vbM7{`LCz` zWiD;-y4o7WK=tI`rdzL$_*+4-65X}HkeB-<69H&{&`|=kCRYYlIS-m-0xAQ5Wln=A zZ)T1ldk`g(7aT!Qa=>K?|8o1F{l_N`r>#e$URM73v$Z7<9l>WQ0u6Fmb5+yi!Y}=_ z8qOSV^52zuP-dbjN>W8(W?-_zzR!3Hf4Q)XrvIuU=_SM z*<3I{af3n%t(q-EAW??@<%HuObNx;oQQRaO@@r9=|1u^-6KleS-v>K6;4DBeFdPP2 zRlHDM!6+nB90?}lYlU* Void)? let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in setPresentationCall?(call) - }, navigateToChat: { accountId, peerId, messageId in - self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil) + }, navigateToChat: { accountId, peerId, messageId, alwaysKeepMessageId in + self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil, alwaysKeepMessageId: alwaysKeepMessageId) }, displayUpgradeProgress: { progress in if let progress = progress { if self.dataImportSplash == nil { @@ -2736,7 +2736,7 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) |> deliverOnMainQueue @@ -2755,7 +2755,7 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { context in - context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny) + context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny, alwaysKeepMessageId: alwaysKeepMessageId) })) } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 1f0026c3e4..f27d52dbb9 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -896,7 +896,7 @@ final class AuthorizedApplicationContext { })) } - func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) { + func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false, alwaysKeepMessageId: Bool = false) { if let storyId { var controllers = self.rootController.viewControllers controllers = controllers.filter { c in @@ -950,7 +950,7 @@ final class AuthorizedApplicationContext { if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController { self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil) } else { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: alwaysKeepMessageId || isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) } }) } diff --git a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift index 89f69e5ddb..670abb783b 100644 --- a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift +++ b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift @@ -39,7 +39,7 @@ public func makeTempContext( firebaseSecretStream: .never(), setNotificationCall: { _ in }, - navigateToChat: { _, _, _ in + navigateToChat: { _, _, _, _ in }, displayUpgradeProgress: { _ in }, appDelegate: nil diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 973a939726..5fa8c6edf8 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -140,7 +140,7 @@ public final class NotificationViewControllerImpl { return nil }) - sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), externalRequestVerificationStream: .never(), externalRecaptchaRequestVerification: { _, _ in return .never() }, autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, appDelegate: nil) + sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), externalRequestVerificationStream: .never(), externalRecaptchaRequestVerification: { _, _ in return .never() }, autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _, _ in }, appDelegate: nil) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 49c0dcc2e9..474a9c21be 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -133,7 +133,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } - private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void + private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?, Bool) -> Void private let apsNotificationToken: Signal private let voipNotificationToken: Signal @@ -268,7 +268,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let energyUsageAutomaticDisposable = MetaDisposable() - init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) { + init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -1760,7 +1760,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) { - self.navigateToChatImpl(accountId, peerId, messageId) + self.navigateToChatImpl(accountId, peerId, messageId, true) } public func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> { From 9b062fd5b8f1d76a7f8bf124e62179926c6d0ae7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 24 Apr 2025 12:49:22 +0400 Subject: [PATCH 04/10] Update API --- .../Sources/ApiUtils/ApiGroupOrChannel.swift | 3 +++ .../SyncCore/SyncCore_TelegramChannel.swift | 1 + .../TelegramEngine/Data/PeersData.swift | 27 +++++++++++++++++++ .../TelegramEngine/Peers/AutoTranslate.swift | 19 +++++++++++++ .../Peers/TelegramEnginePeers.swift | 4 +++ 5 files changed, 54 insertions(+) create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Peers/AutoTranslate.swift diff --git a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift index 122d2f95a5..dc9280893c 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/ApiGroupOrChannel.swift @@ -131,6 +131,9 @@ func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? { if (flags & Int32(1 << 30)) != 0 { channelFlags.insert(.isForum) } + if (flags2 & Int32(1 << 15)) != 0 { + channelFlags.insert(.autoTranslateEnabled) + } var storiesHidden: Bool? if flags2 & (1 << 2) == 0 { // stories_hidden_min diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift index 77c9b3d4ca..6fb4634d4f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChannel.swift @@ -181,6 +181,7 @@ public struct TelegramChannelFlags: OptionSet { public static let joinToSend = TelegramChannelFlags(rawValue: 1 << 9) public static let requestToJoin = TelegramChannelFlags(rawValue: 1 << 10) public static let isForum = TelegramChannelFlags(rawValue: 1 << 11) + public static let autoTranslateEnabled = TelegramChannelFlags(rawValue: 1 << 12) } public final class TelegramChannel: Peer, Equatable { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index f5738a162c..36f3679bed 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -2467,5 +2467,32 @@ public extension TelegramEngine.EngineData.Item { } } } + + public struct AutoTranslateEnabled: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { + public typealias Result = Bool + + fileprivate var id: EnginePeer.Id + public var mapKey: EnginePeer.Id { + return self.id + } + + public init(id: EnginePeer.Id) { + self.id = id + } + + var key: PostboxViewKey { + return .peer(peerId: self.id, components: []) + } + + func extract(view: PostboxView) -> Result { + guard let view = view as? PeerView else { + preconditionFailure() + } + if let channel = peerViewMainPeer(view) as? TelegramChannel { + return channel.flags.contains(.autoTranslateEnabled) + } + return false + } + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/AutoTranslate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AutoTranslate.swift new file mode 100644 index 0000000000..cd513ab5f1 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/AutoTranslate.swift @@ -0,0 +1,19 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +func _internal_toggleAutoTranslation(account: Account, peerId: PeerId, enabled: Bool) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId), let inputChannel = apiInputChannel(peer) { + return account.network.request(Api.functions.channels.toggleAutotranslation(channel: inputChannel, enabled: enabled ? .boolTrue : .boolFalse)) |> `catch` { _ in .complete() } |> map { updates -> Void in + account.stateManager.addUpdates(updates) + } + } else { + return .complete() + } + } + |> switchToLatest + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index a8b746b691..06ac1460c1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -1693,6 +1693,10 @@ public extension TelegramEngine { let _ = _internal_removeChatManagingBot(account: self.account, chatId: chatId).startStandalone() } + public func toggleAutoTranslation(peerId: EnginePeer.Id, enabled: Bool) -> Signal { + return _internal_toggleAutoTranslation(account: self.account, peerId: peerId, enabled: enabled) + } + public func resolveMessageLink(slug: String) -> Signal { return self.account.network.request(Api.functions.account.resolveBusinessChatLink(slug: slug)) |> map(Optional.init) From 999f8c80325f6d2a8773cae9f3319f50cf5c5614 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 24 Apr 2025 14:49:17 +0400 Subject: [PATCH 05/10] Various fixes --- .../Payments/BotPaymentForm.swift | 6 ----- .../TelegramEngine/Payments/StarGifts.swift | 16 +++++------ .../Sources/MediaEditorScreen.swift | 4 ++- .../Sources/PeerInfoScreen.swift | 11 ++++---- .../Chat/ChatControllerLoadDisplayNode.swift | 9 +++++-- .../TranslateUI/Sources/ChatTranslation.swift | 27 +++++++++++++++---- 6 files changed, 46 insertions(+), 27 deletions(-) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 9330c8db39..9abcd3c23f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -179,7 +179,6 @@ public enum BotPaymentFormRequestError { case alreadyActive case noPaymentNeeded case disallowedStarGift - case starGiftResellTooEarly(Int32) } extension BotPaymentInvoice { @@ -483,11 +482,6 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw return .fail(.noPaymentNeeded) } else if error.errorDescription == "USER_DISALLOWED_STARGIFTS" { return .fail(.disallowedStarGift) - } else if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") { - let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...]) - if let value = Int32(timeout) { - return .fail(.starGiftResellTooEarly(value)) - } } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 138a3a7b22..72145ceb8b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -847,14 +847,13 @@ public enum TransferStarGiftError { public enum BuyStarGiftError { case generic - case starGiftResellTooEarly(Int32) } public enum UpdateStarGiftPriceError { case generic + case starGiftResellTooEarly(Int32) } - public enum UpgradeStarGiftError { case generic } @@ -864,12 +863,7 @@ func _internal_buyStarGift(account: Account, slug: String, peerId: EnginePeer.Id return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil) |> map(Optional.init) |> `catch` { error -> Signal in - switch error { - case let .starGiftResellTooEarly(value): - return .fail(.starGiftResellTooEarly(value)) - default: - return .fail(.generic) - } + return .fail(.generic) } |> mapToSignal { paymentForm in if let paymentForm { @@ -2325,6 +2319,12 @@ func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftRe } return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0)) |> mapError { error -> UpdateStarGiftPriceError in + if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") { + let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...]) + if let value = Int32(timeout) { + return .starGiftResellTooEarly(value) + } + } return .generic } |> mapToSignal { updates -> Signal in diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 7e4eac1d55..418b8257c4 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -8155,7 +8155,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration)) var editingItem = EditingItem(asset: asset) - editingItem.caption = self.node.getCaption() + if i == 0 { + editingItem.caption = self.node.getCaption() + } editingItem.values = trimmedValues multipleItems.append(editingItem) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index fc4b92105e..dca4602cbf 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -2007,7 +2007,7 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese return result } -private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatLocation: ChatLocation, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] { +private func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, state: PeerInfoState, chatLocation: ChatLocation, context: AccountContext, presentationData: PresentationData, interaction: PeerInfoInteraction) -> [(AnyHashable, [PeerInfoScreenItem])] { enum Section: Int, CaseIterable { case notifications case groupLocation @@ -2276,11 +2276,12 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL interaction.editingOpenNameColorSetup() })) + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) var isLocked = true - if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel >= 3 { + if let boostLevel = boostStatus?.level, boostLevel >= BoostSubject.autoTranslate.requiredLevel(group: false, context: context, configuration: premiumConfiguration) { isLocked = false } - items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemPeerAutoTranslate, text: presentationData.strings.Channel_Info_AutoTranslate, value: false, icon: UIImage(bundleImageName: "Settings/Menu/AutoTranslate"), isLocked: isLocked, toggled: { value in + items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemPeerAutoTranslate, text: presentationData.strings.Channel_Info_AutoTranslate, value: channel.flags.contains(.autoTranslateEnabled), icon: UIImage(bundleImageName: "Settings/Menu/AutoTranslate"), isLocked: isLocked, toggled: { value in if isLocked { interaction.displayAutoTranslateLocked() } else { @@ -9157,7 +9158,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } private func toggleAutoTranslate(isEnabled: Bool) { - + self.activeActionDisposable.set(self.context.engine.peers.toggleAutoTranslation(peerId: self.peerId, enabled: isEnabled).start()) } private func displayAutoTranslateLocked() { @@ -11918,7 +11919,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } var validEditingSections: [AnyHashable] = [] - let editItems = (self.isSettings || self.isMyProfile) ? settingsEditingItems(data: self.data, state: self.state, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isMyProfile: self.isMyProfile) : editingItems(data: self.data, state: self.state, chatLocation: self.chatLocation, context: self.context, presentationData: self.presentationData, interaction: self.interaction) + let editItems = (self.isSettings || self.isMyProfile) ? settingsEditingItems(data: self.data, state: self.state, context: self.context, presentationData: self.presentationData, interaction: self.interaction, isMyProfile: self.isMyProfile) : editingItems(data: self.data, boostStatus: self.boostStatus, state: self.state, chatLocation: self.chatLocation, context: self.context, presentationData: self.presentationData, interaction: self.interaction) for (sectionId, sectionItems) in editItems { var insets = UIEdgeInsets() diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index b724c12034..a665b1b70d 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -675,17 +675,22 @@ extension ChatControllerImpl { let isHidden = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.TranslationHidden(id: peerId)) |> distinctUntilChanged + + let hasAutoTranslate = self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId)) + |> distinctUntilChanged + self.translationStateDisposable = (combineLatest( queue: .concurrentDefaultQueue(), isPremium, isHidden, + hasAutoTranslate, ApplicationSpecificNotice.translationSuggestion(accountManager: self.context.sharedContext.accountManager) - ) |> mapToSignal { isPremium, isHidden, counterAndTimestamp -> Signal in + ) |> mapToSignal { isPremium, isHidden, hasAutoTranslate, counterAndTimestamp -> Signal in var maybeSuggestPremium = false if counterAndTimestamp.0 >= 3 { maybeSuggestPremium = true } - if (isPremium || maybeSuggestPremium) && !isHidden { + if (isPremium || maybeSuggestPremium || hasAutoTranslate) && !isHidden { return chatTranslationState(context: context, peerId: peerId) |> map { translationState -> ChatPresentationTranslationState? in if let translationState, !translationState.fromLang.isEmpty && (translationState.fromLang != baseLanguageCode || translationState.isEnabled) { diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index f6bdf92ec2..9111cbf26e 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -180,10 +180,17 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) baseLang = String(baseLang.dropLast(rawSuffix.count)) } - return context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) - |> mapToSignal { sharedData in - let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings - if !settings.translateChats { + + + return combineLatest( + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings]) + |> map { sharedData -> TranslationSettings in + return sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) ?? TranslationSettings.defaultSettings + }, + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.AutoTranslateEnabled(id: peerId)) + ) + |> mapToSignal { settings, autoTranslateEnabled in + if !settings.translateChats && !autoTranslateEnabled { return .single(nil) } @@ -286,12 +293,22 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id) if loggingEnabled { Logger.shared.log("ChatTranslation", "Ended with: \(fromLang)") } + + let isEnabled: Bool + if let currentIsEnabled = cached?.isEnabled { + isEnabled = currentIsEnabled + } else if autoTranslateEnabled { + isEnabled = true + } else { + isEnabled = false + } + let state = ChatTranslationState( baseLang: baseLang, fromLang: fromLang, timestamp: currentTime, toLang: cached?.toLang, - isEnabled: cached?.isEnabled ?? false + isEnabled: isEnabled ) let _ = updateChatTranslationState(engine: context.engine, peerId: peerId, state: state).start() if !dontTranslateLanguages.contains(fromLang) { From d5139b63635772dedf082af08bb46429e16b0717 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 24 Apr 2025 15:06:21 +0400 Subject: [PATCH 06/10] Various fixes --- .../Sources/MediaEditorScreen.swift | 34 ++++++++++++++++--- .../Sources/MediaScrubberComponent.swift | 2 +- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 418b8257c4..0af513e356 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2021,8 +2021,12 @@ final class MediaEditorScreenComponent: Component { if case .avatarEditor = controller.mode { maxDuration = 9.9 } else { - maxDuration = storyMaxCombinedVideoDuration - segmentDuration = storyMaxVideoDuration + if controller.node.items.count > 0 { + maxDuration = storyMaxVideoDuration + } else { + maxDuration = storyMaxCombinedVideoDuration + segmentDuration = storyMaxVideoDuration + } } } @@ -3506,7 +3510,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID values: initialValues, hasHistogram: true ) - mediaEditor.maxDuration = storyMaxCombinedVideoDuration + if case .storyEditor = controller.mode, self.items.isEmpty { + mediaEditor.maxDuration = storyMaxCombinedVideoDuration + } + if case .avatarEditor = controller.mode { mediaEditor.setVideoIsMuted(true) } else if case let .coverEditor(dimensions) = controller.mode { @@ -6801,9 +6808,26 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID coverImage = nil } + var storyCount: Int32 = 0 + if self.node.items.count > 0 { + storyCount = Int32(self.node.items.count(where: { $0.isEnabled })) + } else { + if case let .asset(asset) = self.node.subject { + let duration: Double + if let playerDuration = mediaEditor.duration { + duration = playerDuration + } else { + duration = asset.duration + } + if duration > storyMaxVideoDuration { + storyCount = Int32(min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))) + } + } + } + let stateContext = ShareWithPeersScreen.StateContext( context: self.context, - subject: .stories(editing: false, count: Int32(self.node.items.count(where: { $0.isEnabled }))), + subject: .stories(editing: false, count: storyCount), editing: false, initialPeerIds: Set(privacy.privacy.additionallyIncludePeers), closeFriends: self.closeFriends.get(), @@ -8151,7 +8175,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration))) var start = values.videoTrimRange?.lowerBound ?? 0 - for _ in 0 ..< storyCount { + for i in 0 ..< storyCount { let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration)) var editingItem = EditingItem(asset: asset) diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index f7847fee37..7793a3f9ff 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -1510,7 +1510,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega var validIds = Set() var segmentFrame = CGRect(x: segmentOrigin + segmentWidth, y: 0.0, width: 1.0, height: containerFrame.size.height) - for i in 0 ..< segmentCount { + for i in 0 ..< min(segmentCount, 2) { let id = Int32(i) validIds.insert(id) From 9483448aa366e6d1549687ba218aba5d48526085 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 24 Apr 2025 17:53:39 +0400 Subject: [PATCH 07/10] Update API --- .../Telegram-iOS/en.lproj/Localizable.strings | 5 +++ submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api15.swift | 22 ++++++---- submodules/TelegramApi/Sources/Api23.swift | 22 ++++++---- .../ApiUtils/TelegramMediaAction.swift | 4 +- .../SyncCore_TelegramMediaAction.swift | 18 +++++++-- .../TelegramEngine/Payments/StarGifts.swift | 40 +++++++++++++++---- .../TelegramEngine/Payments/Stars.swift | 6 ++- .../Sources/ServiceMessageStrings.swift | 22 ++++++++-- .../ChatMessageGiftBubbleContentNode.swift | 2 +- .../Sources/GiftViewScreen.swift | 2 +- .../Sources/MediaEditorScreen.swift | 2 +- .../ChatInterfaceStateContextMenus.swift | 2 +- 13 files changed, 111 insertions(+), 40 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 16e056f9b0..3ed169d2fb 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14220,3 +14220,8 @@ Sorry for the inconvenience."; "Channel.AdminLog.MessageToggleAutoTranslateOn" = "%@ enabled autotranslation of messages"; "Channel.AdminLog.MessageToggleAutoTranslateOff" = "%@ disabled autotranslation of messages"; + +"Notification.StarsGift.Bought" = "%1$@ gifted you %2$@ for %3$@"; +"Notification.StarsGift.Bought.Stars_1" = "%@ Star"; +"Notification.StarsGift.Bought.Stars_any" = "%@ Stars"; +"Notification.StarsGift.BoughtYou" = "You gifted %1$@ for %2$@"; diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 6d25bfee71..0e0456ca4d 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -603,7 +603,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) } dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } dict[1192749220] = { return Api.MessageAction.parse_messageActionStarGift($0) } - dict[1600878025] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } + dict[775611918] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } dict[228168278] = { return Api.MessageAction.parse_messageActionTopicCreate($0) } dict[-1064024032] = { return Api.MessageAction.parse_messageActionTopicEdit($0) } @@ -872,7 +872,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[289586518] = { return Api.SavedContact.parse_savedPhoneContact($0) } dict[-1115174036] = { return Api.SavedDialog.parse_savedDialog($0) } dict[-881854424] = { return Api.SavedReactionTag.parse_savedReactionTag($0) } - dict[1616305061] = { return Api.SavedStarGift.parse_savedStarGift($0) } + dict[-539360103] = { return Api.SavedStarGift.parse_savedStarGift($0) } dict[-911191137] = { return Api.SearchResultsCalendarPeriod.parse_searchResultsCalendarPeriod($0) } dict[2137295719] = { return Api.SearchResultsPosition.parse_searchResultPosition($0) } dict[871426631] = { return Api.SecureCredentialsEncrypted.parse_secureCredentialsEncrypted($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 33eec1e2b1..651700542b 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -381,7 +381,7 @@ public extension Api { case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper) case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?) case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?) - case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?) + case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?) case messageActionSuggestProfilePhoto(photo: Api.Photo) case messageActionTopicCreate(flags: Int32, title: String, iconColor: Int32, iconEmojiId: Int64?) case messageActionTopicEdit(flags: Int32, title: String?, iconEmojiId: Int64?, closed: Api.Bool?, hidden: Api.Bool?) @@ -767,9 +767,9 @@ public extension Api { if Int(flags) & Int(1 << 12) != 0 {peer!.serialize(buffer, true)} if Int(flags) & Int(1 << 12) != 0 {serializeInt64(savedId!, buffer: buffer, boxed: false)} break - case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleStars): + case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleStars, let canTransferAt, let canResellAt): if boxed { - buffer.appendInt32(1600878025) + buffer.appendInt32(775611918) } serializeInt32(flags, buffer: buffer, boxed: false) gift.serialize(buffer, true) @@ -779,6 +779,8 @@ public extension Api { if Int(flags) & Int(1 << 7) != 0 {peer!.serialize(buffer, true)} if Int(flags) & Int(1 << 7) != 0 {serializeInt64(savedId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeInt64(resaleStars!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 9) != 0 {serializeInt32(canTransferAt!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 10) != 0 {serializeInt32(canResellAt!, buffer: buffer, boxed: false)} break case .messageActionSuggestProfilePhoto(let photo): if boxed { @@ -913,8 +915,8 @@ public extension Api { return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)]) case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId): return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any)]) - case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleStars): - return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleStars", resaleStars as Any)]) + case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleStars, let canTransferAt, let canResellAt): + return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleStars", resaleStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)]) case .messageActionSuggestProfilePhoto(let photo): return ("messageActionSuggestProfilePhoto", [("photo", photo as Any)]) case .messageActionTopicCreate(let flags, let title, let iconColor, let iconEmojiId): @@ -1675,6 +1677,10 @@ public extension Api { if Int(_1!) & Int(1 << 7) != 0 {_7 = reader.readInt64() } var _8: Int64? if Int(_1!) & Int(1 << 8) != 0 {_8 = reader.readInt64() } + var _9: Int32? + if Int(_1!) & Int(1 << 9) != 0 {_9 = reader.readInt32() } + var _10: Int32? + if Int(_1!) & Int(1 << 10) != 0 {_10 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 3) == 0) || _3 != nil @@ -1683,8 +1689,10 @@ public extension Api { let _c6 = (Int(_1!) & Int(1 << 7) == 0) || _6 != nil let _c7 = (Int(_1!) & Int(1 << 7) == 0) || _7 != nil let _c8 = (Int(_1!) & Int(1 << 8) == 0) || _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.MessageAction.messageActionStarGiftUnique(flags: _1!, gift: _2!, canExportAt: _3, transferStars: _4, fromId: _5, peer: _6, savedId: _7, resaleStars: _8) + let _c9 = (Int(_1!) & Int(1 << 9) == 0) || _9 != nil + let _c10 = (Int(_1!) & Int(1 << 10) == 0) || _10 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { + return Api.MessageAction.messageActionStarGiftUnique(flags: _1!, gift: _2!, canExportAt: _3, transferStars: _4, fromId: _5, peer: _6, savedId: _7, resaleStars: _8, canTransferAt: _9, canResellAt: _10) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 2d1c58a694..cbfbf62bbb 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -144,13 +144,13 @@ public extension Api { } public extension Api { enum SavedStarGift: TypeConstructorDescription { - case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?) + case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars): + case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt): if boxed { - buffer.appendInt32(1616305061) + buffer.appendInt32(-539360103) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {fromId!.serialize(buffer, true)} @@ -163,14 +163,16 @@ public extension Api { if Int(flags) & Int(1 << 6) != 0 {serializeInt64(upgradeStars!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 7) != 0 {serializeInt32(canExportAt!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeInt64(transferStars!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 13) != 0 {serializeInt32(canTransferAt!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 14) != 0 {serializeInt32(canResellAt!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars): - return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any)]) + case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt): + return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any)]) } } @@ -203,6 +205,10 @@ public extension Api { if Int(_1!) & Int(1 << 7) != 0 {_10 = reader.readInt32() } var _11: Int64? if Int(_1!) & Int(1 << 8) != 0 {_11 = reader.readInt64() } + var _12: Int32? + if Int(_1!) & Int(1 << 13) != 0 {_12 = reader.readInt32() } + var _13: Int32? + if Int(_1!) & Int(1 << 14) != 0 {_13 = reader.readInt32() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil @@ -214,8 +220,10 @@ public extension Api { let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil let _c10 = (Int(_1!) & Int(1 << 7) == 0) || _10 != nil let _c11 = (Int(_1!) & Int(1 << 8) == 0) || _11 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 { - return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11) + let _c12 = (Int(_1!) & Int(1 << 13) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 14) == 0) || _13 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { + return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13) } else { return nil diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 2cf9cd110d..4c3e0a445f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -192,11 +192,11 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return nil } return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId)) - case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleStars): + case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleStars, canTransferDate, canResaleDate): guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleStars: resaleStars)) + return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleStars: resaleStars, canTransferDate: canTransferDate, canResaleDate: canResaleDate)) case let .messageActionPaidMessagesRefunded(count, stars): return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) case let .messageActionPaidMessagesPrice(stars): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index e1cf771e0c..ce4796607e 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -156,7 +156,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?) - case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleStars: Int64?) + case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleStars: Int64?, canTransferDate: Int32?, canResaleDate: Int32?) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64) case conferenceCall(ConferenceCall) @@ -283,7 +283,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case 44: self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId")) case 45: - self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleStars: decoder.decodeOptionalInt64ForKey("resaleStars")) + self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleStars: decoder.decodeOptionalInt64ForKey("resaleStars"), canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate")) case 46: self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0)) case 47: @@ -633,7 +633,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "savedId") } - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, peerId, senderId, savedId, resaleStars): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, peerId, senderId, savedId, resaleStars, canTransferDate, canResaleDate): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") encoder.encodeBool(isUpgrade, forKey: "isUpgrade") @@ -670,6 +670,16 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "resaleStars") } + if let canTransferDate { + encoder.encodeInt32(canTransferDate, forKey: "canTransferDate") + } else { + encoder.encodeNil(forKey: "canTransferDate") + } + if let canResaleDate { + encoder.encodeInt32(canResaleDate, forKey: "canResaleDate") + } else { + encoder.encodeNil(forKey: "canResaleDate") + } case let .paidMessagesRefunded(count, stars): encoder.encodeInt32(46, forKey: "_rawValue") encoder.encodeInt32(count, forKey: "count") @@ -723,7 +733,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { peerIds.append(senderId) } return peerIds - case let .starGiftUnique(_, _, _, _, _, _, _, peerId, senderId, _, _): + case let .starGiftUnique(_, _, _, _, _, _, _, peerId, senderId, _, _, _, _): var peerIds: [PeerId] = [] if let peerId { peerIds.append(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 1cdbf93064..67ef375658 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -968,7 +968,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star case let .updateNewMessage(message, _, _): if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _) = action.action, case let .Id(messageId) = message.id { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _, canTransferDate, canResaleDate) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -989,7 +989,9 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star canUpgrade: false, canExportDate: canExportDate, upgradeStars: nil, - transferStars: transferStars + transferStars: transferStars, + canTransferDate: canTransferDate, + canResaleDate: canResaleDate )) } } @@ -1691,6 +1693,8 @@ public final class ProfileGiftsContext { case upgradeStars case transferStars case giftAddress + case canTransferDate + case canResaleDate } public let gift: TelegramCore.StarGift @@ -1707,6 +1711,8 @@ public final class ProfileGiftsContext { public let canExportDate: Int32? public let upgradeStars: Int64? public let transferStars: Int64? + public let canTransferDate: Int32? + public let canResaleDate: Int32? fileprivate let _fromPeerId: EnginePeer.Id? @@ -1728,7 +1734,9 @@ public final class ProfileGiftsContext { canUpgrade: Bool, canExportDate: Int32?, upgradeStars: Int64?, - transferStars: Int64? + transferStars: Int64?, + canTransferDate: Int32?, + canResaleDate: Int32? ) { self.gift = gift self.reference = reference @@ -1745,6 +1753,8 @@ public final class ProfileGiftsContext { self.canExportDate = canExportDate self.upgradeStars = upgradeStars self.transferStars = transferStars + self.canTransferDate = canTransferDate + self.canResaleDate = canResaleDate } public init(from decoder: Decoder) throws { @@ -1771,6 +1781,8 @@ public final class ProfileGiftsContext { self.canExportDate = try container.decodeIfPresent(Int32.self, forKey: .canExportDate) self.upgradeStars = try container.decodeIfPresent(Int64.self, forKey: .upgradeStars) self.transferStars = try container.decodeIfPresent(Int64.self, forKey: .transferStars) + self.canTransferDate = try container.decodeIfPresent(Int32.self, forKey: .canTransferDate) + self.canResaleDate = try container.decodeIfPresent(Int32.self, forKey: .canResaleDate) } public func encode(to encoder: Encoder) throws { @@ -1790,6 +1802,8 @@ public final class ProfileGiftsContext { try container.encodeIfPresent(self.canExportDate, forKey: .canExportDate) try container.encodeIfPresent(self.upgradeStars, forKey: .upgradeStars) try container.encodeIfPresent(self.transferStars, forKey: .transferStars) + try container.encodeIfPresent(self.canTransferDate, forKey: .canTransferDate) + try container.encodeIfPresent(self.canResaleDate, forKey: .canResaleDate) } public func withGift(_ gift: TelegramCore.StarGift) -> StarGift { @@ -1807,7 +1821,9 @@ public final class ProfileGiftsContext { canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, - transferStars: self.transferStars + transferStars: self.transferStars, + canTransferDate: self.canTransferDate, + canResaleDate: self.canResaleDate ) } @@ -1826,7 +1842,9 @@ public final class ProfileGiftsContext { canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, - transferStars: self.transferStars + transferStars: self.transferStars, + canTransferDate: self.canTransferDate, + canResaleDate: self.canResaleDate ) } @@ -1845,7 +1863,9 @@ public final class ProfileGiftsContext { canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, - transferStars: self.transferStars + transferStars: self.transferStars, + canTransferDate: self.canTransferDate, + canResaleDate: self.canResaleDate ) } fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift { @@ -1863,7 +1883,9 @@ public final class ProfileGiftsContext { canUpgrade: self.canUpgrade, canExportDate: self.canExportDate, upgradeStars: self.upgradeStars, - transferStars: self.transferStars + transferStars: self.transferStars, + canTransferDate: self.canTransferDate, + canResaleDate: self.canResaleDate ) } } @@ -2042,7 +2064,7 @@ public final class ProfileGiftsContext { extension ProfileGiftsContext.State.StarGift { init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) { switch apiSavedStarGift { - case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars): + case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt): guard let gift = StarGift(apiStarGift: apiGift) else { return nil } @@ -2086,6 +2108,8 @@ extension ProfileGiftsContext.State.StarGift { self.canExportDate = canExportDate self.upgradeStars = upgradeStars self.transferStars = transferStars + self.canTransferDate = canTransferAt + self.canResaleDate = canResaleAt } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 59e0d2849f..1027f65318 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1527,7 +1527,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale: receiptMessageId = nil } - } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _) = action.action, case let .Id(messageId) = message.id { + } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, peerId, _, savedId, _, canTransferDate, canResaleDate) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -1548,7 +1548,9 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot canUpgrade: false, canExportDate: canExportDate, upgradeStars: nil, - transferStars: transferStars + transferStars: transferStars, + canTransferDate: canTransferDate, + canResaleDate: canResaleDate ) } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 49ecafc847..f83833adcc 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1165,7 +1165,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, peerId, senderId, _, resaleStars, _, _): if case let .unique(gift) = gift { if !forAdditionalServiceMessage && !"".isEmpty { attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor) @@ -1188,7 +1188,13 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, if message.id.peerId.isTelegramNotifications && senderId == nil { attributedString = NSAttributedString(string: strings.Notification_StarsGift_SentSomeone, font: titleFont, textColor: primaryTextColor) } else if message.author?.id == accountPeerId { - attributedString = NSAttributedString(string: strings.Notification_StarsGift_TransferYou, font: titleFont, textColor: primaryTextColor) + if let resaleStars { + let starsString = strings.Notification_StarsGift_Bought_Stars(Int32(resaleStars)) + let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_BoughtYou(giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) + } else { + attributedString = NSAttributedString(string: strings.Notification_StarsGift_TransferYou, font: titleFont, textColor: primaryTextColor) + } } else if let senderId, let peer = message.peers[senderId] { if let peerId, let targetPeer = message.peers[peerId] { if senderId == accountPeerId { @@ -1210,8 +1216,16 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Transfer(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } else { - let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) - attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Transfer(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + if let resaleStars { + let starsString = strings.Notification_StarsGift_Bought_Stars(Int32(resaleStars)) + let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Bought(peerName, giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Transfer(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index b0cc08742c..75c0a68835 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -560,7 +560,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_StarGift_View } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _): if case let .unique(uniqueGift) = gift { isStarGift = true diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 802039c050..e4b0550757 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -2841,7 +2841,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { reference = .message(messageId: message.id) } return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId) - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, _, _): var reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 0af513e356..bf319c7e24 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3647,7 +3647,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil, canTransferDate: nil, canResaleDate: nil))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index a6cee571bb..fd18d86fe5 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1124,7 +1124,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let sendGiftTitle: String var isIncoming = message.effectivelyIncoming(context.account.peerId) for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _) = action.action { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _) = action.action { if isUpgrade && message.author?.id == context.account.peerId { isIncoming = true } From c4731d8b8dc23b20da54bb275f3b791e8de1b6a7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 24 Apr 2025 21:12:40 +0400 Subject: [PATCH 08/10] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 65 ++++++++++++++ .../Sources/MediaPickerScreen.swift | 8 +- .../MtProtoKit/Sources/MTApiEnvironment.m | 2 + .../TelegramEngine/Payments/StarGifts.swift | 29 ++++-- .../Sources/ServiceMessageStrings.swift | 8 +- .../Sources/GiftSetupScreen.swift | 4 +- .../GiftAttributeListContextItem.swift | 11 +-- .../Sources/GiftStoreScreen.swift | 89 +++++-------------- .../Sources/GiftViewScreen.swift | 44 ++++----- .../Sources/PeerInfoGiftsPaneNode.swift | 3 +- .../Sources/StarsWithdrawalScreen.swift | 19 ++-- .../DeviceModel/Sources/DeviceModel.swift | 5 ++ .../WebAppSecureStorageTransferScreen.swift | 1 - 13 files changed, 164 insertions(+), 124 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 3ed169d2fb..e8b14c3c91 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14225,3 +14225,68 @@ Sorry for the inconvenience."; "Notification.StarsGift.Bought.Stars_1" = "%@ Star"; "Notification.StarsGift.Bought.Stars_any" = "%@ Stars"; "Notification.StarsGift.BoughtYou" = "You gifted %1$@ for %2$@"; +"Notification.StarsGift.BoughtForYouself" = "You bought this gift for %1$@"; + +"Gift.View.Context.ChangePrice" = "Change Price"; +"Gift.View.Context.ViewInProfile" = "View in Profile"; + +"Gift.View.Sell" = "sell"; +"Gift.View.Unlist" = "unlist"; + +"Gift.View.BuyFor" = "Buy for"; +"Gift.View.SellingGiftInfo" = "%@ is selling this gift and you can buy it."; + +"Gift.View.Resale.Success.Title" = "Gift Sent"; +"Gift.View.Resale.Success.Text" = "%@ has been notified about your gift."; +"Gift.View.Resale.SuccessYou.Title" = "Gift Acquired"; +"Gift.View.Resale.SuccessYou.Text" = "%@ is now yours."; + +"Gift.View.Resale.Unlist.Title" = "Unlist This Item?"; +"Gift.View.Resale.Unlist.Text" = "It will no longer be for sale."; +"Gift.View.Resale.Unlist.Unlist" = "Unlist"; +"Gift.View.Resale.Unlist.Success" = "%@ is removed from sale."; +"Gift.View.Resale.List.Success" = "%@ is now for sale!"; +"Gift.View.Resale.Relist.Success" = "%1$@ is relisted for %2$@."; +"Gift.View.Resale.Relist.Success.Stars_1" = "%@ Star"; +"Gift.View.Resale.Relist.Success.Stars_any" = "%@ Stars"; + +"Stars.SellGift.Title" = "Sell Gift"; +"Stars.SellGift.EditTitle" = "Edit Price"; +"Stars.SellGift.AmountTitle" = "PRICE IN STARS"; +"Stars.SellGift.AmountPlaceholder" = "Enter Price"; +"Stars.SellGift.AmountInfo" = "You will receive **%@**."; +"Stars.SellGift.AmountInfo.Stars_1" = "%@ Star"; +"Stars.SellGift.AmountInfo.Stars_any" = "%@ Stars"; +"Stars.SellGift.Sell" = "Sell"; +"Stars.SellGift.SellFor" = "Sell for"; + +"PeerInfo.Gifts.Sale" = "sale"; + +"Gift.Store.ForResale_1" = "%@ for resale"; +"Gift.Store.ForResale_any" = "%@ for resale"; +"Gift.Store.Sort.Price" = "Price"; +"Gift.Store.Sort.Date" = "Date"; +"Gift.Store.Sort.Number" = "Number"; +"Gift.Store.SortByPrice" = "Sort By Price"; +"Gift.Store.SortByDate" = "Sort By Date"; +"Gift.Store.SortByNumber" = "Sort By Number"; +"Gift.Store.Filter.Model" = "Model"; +"Gift.Store.Filter.Backdrop" = "Backdrop"; +"Gift.Store.Filter.Symbol" = "Symbol"; +"Gift.Store.Filter.Selected.Model_1" = "%@ Model"; +"Gift.Store.Filter.Selected.Model_any" = "%@ Models"; +"Gift.Store.Filter.Selected.Backdrop_1" = "%@ Backdrop"; +"Gift.Store.Filter.Selected.Backdrop_any" = "%@ Backdrops"; +"Gift.Store.Filter.Selected.Symbol_1" = "%@ Symbol"; +"Gift.Store.Filter.Selected.Symbol_any" = "%@ Symbols"; +"Gift.Store.Search" = "Search"; +"Gift.Store.SelectAll" = "Select All"; +"Gift.Store.NoResults" = "No Results"; +"Gift.Store.EmptyResults" = "No Matching Gifts"; +"Gift.Store.ClearFilters" = "Clear Filters"; + +"Gift.Send.AvailableForResale" = "Available for Resale"; + +"MediaPicker.CreateStory_1" = "Create %@ Story"; +"MediaPicker.CreateStory_any" = "Create %@ Stories"; +"MediaPicker.CombineIntoCollage" = "Combine into Collage"; diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 9ea769f836..03575c9fc2 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -2395,15 +2395,11 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1) if case .assets(_, .story) = self.subject, self.selectionCount > 0 { - //TODO:localize - var text = "Create 1 Story" - if self.selectionCount > 1 { - text = "Create \(self.selectionCount) Stories" - } + let text = self.presentationData.strings.MediaPicker_CreateStory(self.selectionCount) self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top))) if self.selectionCount > 1 && self.selectionCount <= 6 { - self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom))) + self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: self.presentationData.strings.MediaPicker_CombineIntoCollage, badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom))) } else { self.secondaryButtonStatePromise.set(.single(nil)) } diff --git a/submodules/MtProtoKit/Sources/MTApiEnvironment.m b/submodules/MtProtoKit/Sources/MTApiEnvironment.m index 399acc5097..a7eea0c54c 100644 --- a/submodules/MtProtoKit/Sources/MTApiEnvironment.m +++ b/submodules/MtProtoKit/Sources/MTApiEnvironment.m @@ -542,6 +542,8 @@ NSString *suffix = @""; return @"iPhone 16 Pro"; if ([platform isEqualToString:@"iPhone17,2"]) return @"iPhone 16 Pro Max"; + if ([platform isEqualToString:@"iPhone17,5"]) + return @"iPhone 16e"; if ([platform hasPrefix:@"iPod1"]) return @"iPod touch 1G"; diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 67ef375658..32aa9cdc41 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -1534,13 +1534,28 @@ private final class ProfileGiftsContextImpl { guard let self else { return EmptyDisposable } + + var saveToProfile = false + if let gift = self.gifts.first(where: { $0.reference == reference }) { + if !gift.savedToProfile { + saveToProfile = true + } + } else if let gift = self.filteredGifts.first(where: { $0.reference == reference }) { + if !gift.savedToProfile { + saveToProfile = true + } + } + + var signal = _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price) + if saveToProfile { + signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true) + |> castError(UpdateStarGiftPriceError.self) + |> then(signal) + } + let disposable = MetaDisposable() disposable.set( - (_internal_updateStarGiftResalePrice( - account: self.account, - reference: reference, - price: price - ) + (signal |> deliverOn(self.queue)).startStrict(error: { error in subscriber.putError(error) }, completed: { @@ -1562,7 +1577,7 @@ private final class ProfileGiftsContextImpl { }) { if case let .unique(uniqueGift) = self.gifts[index].gift { let updatedUniqueGift = uniqueGift.withResellStars(price) - let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)) + let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true) self.gifts[index] = updatedGift } } @@ -1585,7 +1600,7 @@ private final class ProfileGiftsContextImpl { }) { if case let .unique(uniqueGift) = self.filteredGifts[index].gift { let updatedUniqueGift = uniqueGift.withResellStars(price) - let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)) + let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)).withSavedToProfile(true) self.filteredGifts[index] = updatedGift } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index f83833adcc..812d3a26ff 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1190,8 +1190,12 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } else if message.author?.id == accountPeerId { if let resaleStars { let starsString = strings.Notification_StarsGift_Bought_Stars(Int32(resaleStars)) - let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" - attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_BoughtYou(giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) + if message.id.peerId == accountPeerId { + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_BoughtForYouself(starsString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes]) + } else { + let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_BoughtYou(giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes]) + } } else { attributedString = NSAttributedString(string: strings.Notification_StarsGift_TransferYou, font: titleFont, textColor: primaryTextColor) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index d4c2e9837a..3c325c54bb 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -801,7 +801,7 @@ final class GiftSetupScreenComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "Available for Resale", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)) + text: .plain(NSAttributedString(string: environment.strings.Gift_Send_AvailableForResale, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)) ) )), ], alignment: .left, spacing: 2.0)), @@ -1770,6 +1770,8 @@ public final class GiftSetupScreen: ViewControllerComponentContainer { self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.context.sharedContext.currentPresentationData.with { $0 }.strings.Common_Back, style: .plain, target: nil, action: nil) + self.scrollToTop = { [weak self] in guard let self, let componentView = self.node.hostView.componentView as? GiftSetupScreenComponent.View else { return diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift index a55122afa6..bbb4658b85 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftAttributeListContextItem.swift @@ -218,8 +218,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu let selectedAttributes = Set(item.selectedAttributes) - //TODO:localize - let selectAllAction = ContextMenuActionItem(text: "Select All", icon: { theme in + let selectAllAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_SelectAll, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, iconPosition: .left, action: { _, f in getController()?.dismiss(result: .dismissWithoutContent, completion: nil) @@ -248,7 +247,7 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu } let nopAction: ((ContextControllerProtocol?, @escaping (ContextMenuActionResult) -> Void) -> Void)? = nil - let emptyResultsAction = ContextMenuActionItem(text: "No Results", textFont: .small, icon: { _ in return nil }, action: nopAction) + let emptyResultsAction = ContextMenuActionItem(text: presentationData.strings.Gift_Store_NoResults, textFont: .small, icon: { _ in return nil }, action: nopAction) let emptyResultsActionNode = ContextControllerActionsListActionItemNode(context: item.context, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: emptyResultsAction) actionNodes.append(emptyResultsActionNode) @@ -412,12 +411,6 @@ private final class GiftAttributeListContextItemNode: ASDisplayNode, ContextMenu } func actionNode(at point: CGPoint) -> ContextActionNodeProtocol { -// for actionNode in self.actionNodes { -// let frame = actionNode.convert(actionNode.bounds, to: self) -// if frame.contains(point) { -// return actionNode -// } -// } return self } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 0b2912e02c..8ac35acf59 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -302,7 +302,7 @@ final class GiftStoreScreenComponent: Component { PlainButtonComponent( content: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)), + text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ClearFilters, font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)), horizontalAlignment: .center, maximumNumberOfLines: 0 ) @@ -349,7 +349,7 @@ final class GiftStoreScreenComponent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.Gift_Store_EmptyResults, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center ) ), @@ -441,21 +441,21 @@ final class GiftStoreScreenComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] - items.append(.action(ContextMenuActionItem(text: "Sort by Price", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByPrice, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortValue"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.state?.starGiftsContext.updateSorting(.value) }))) - items.append(.action(ContextMenuActionItem(text: "Sort by Date", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByDate, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortDate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) self?.state?.starGiftsContext.updateSorting(.date) }))) - items.append(.action(ContextMenuActionItem(text: "Sort by Number", icon: { theme in + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_Store_SortByNumber, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/SortNumber"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -493,12 +493,11 @@ final class GiftStoreScreenComponent: Component { } } - //TODO:localize var items: [ContextMenuItem] = [] if modelAttributes.count >= 8 { items.append(.custom(SearchContextItem( context: component.context, - placeholder: "Search", + placeholder: presentationData.strings.Gift_Store_Search, value: "", valueChanged: { value in searchQueryPromise.set(value) @@ -585,12 +584,11 @@ final class GiftStoreScreenComponent: Component { } } - //TODO:localize var items: [ContextMenuItem] = [] if backdropAttributes.count >= 8 { items.append(.custom(SearchContextItem( context: component.context, - placeholder: "Search", + placeholder: presentationData.strings.Gift_Store_Search, value: "", valueChanged: { value in searchQueryPromise.set(value) @@ -677,12 +675,11 @@ final class GiftStoreScreenComponent: Component { } } - //TODO:localize var items: [ContextMenuItem] = [] if patternAttributes.count >= 8 { items.append(.custom(SearchContextItem( context: component.context, - placeholder: "Search", + placeholder: presentationData.strings.Gift_Store_Search, value: "", valueChanged: { value in searchQueryPromise.set(value) @@ -814,35 +811,7 @@ final class GiftStoreScreenComponent: Component { transition.setFrame(view: topPanelView, frame: topPanelFrame) transition.setFrame(view: topSeparatorView, frame: topSeparatorFrame) } - -// let cancelButtonSize = self.cancelButton.update( -// transition: transition, -// component: AnyComponent( -// PlainButtonComponent( -// content: AnyComponent( -// MultilineTextComponent( -// text: .plain(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor)), -// horizontalAlignment: .center -// ) -// ), -// effectAlignment: .center, -// action: { -// controller()?.dismiss() -// }, -// animateScale: false -// ) -// ), -// environment: {}, -// containerSize: CGSize(width: availableSize.width, height: 100.0) -// ) -// let cancelButtonFrame = CGRect(origin: CGPoint(x: environment.safeInsets.left + 16.0, y: environment.statusBarHeight + (environment.navigationHeight - environment.statusBarHeight) / 2.0 - cancelButtonSize.height / 2.0), size: cancelButtonSize) -// if let cancelButtonView = self.cancelButton.view { -// if cancelButtonView.superview == nil { -// self.addSubview(cancelButtonView) -// } -// transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame) -// } - + let balanceTitleSize = self.balanceTitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( @@ -922,7 +891,7 @@ final class GiftStoreScreenComponent: Component { let subtitleSize = self.subtitle.update( transition: transition, component: AnyComponent(BalancedTextComponent( - text: .plain(NSAttributedString(string: "\(effectiveCount) for resale", font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.Gift_Store_ForResale(effectiveCount), font: Font.regular(13.0), textColor: theme.rootController.navigationBar.secondaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 1 )), @@ -940,18 +909,18 @@ final class GiftStoreScreenComponent: Component { let optionSpacing: CGFloat = 10.0 let optionWidth = (availableSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 - var sortingTitle = "Date" + var sortingTitle = environment.strings.Gift_Store_Sort_Date var sortingIcon: String = "Peer Info/SortDate" if let sorting = self.state?.starGiftsState?.sorting { switch sorting { case .date: - sortingTitle = "Date" + sortingTitle = environment.strings.Gift_Store_Sort_Date sortingIcon = "Peer Info/SortDate" case .value: - sortingTitle = "Price" + sortingTitle = environment.strings.Gift_Store_Sort_Price sortingIcon = "Peer Info/SortValue" case .number: - sortingTitle = "Number" + sortingTitle = environment.strings.Gift_Store_Sort_Number sortingIcon = "Peer Info/SortNumber" } } @@ -968,13 +937,13 @@ final class GiftStoreScreenComponent: Component { } )) - var modelTitle = "Model" - var backdropTitle = "Backdrop" - var symbolTitle = "Symbol" + var modelTitle = environment.strings.Gift_Store_Filter_Model + var backdropTitle = environment.strings.Gift_Store_Filter_Backdrop + var symbolTitle = environment.strings.Gift_Store_Filter_Symbol if let filterAttributes = self.state?.starGiftsState?.filterAttributes { - var modelCount = 0 - var backdropCount = 0 - var symbolCount = 0 + var modelCount: Int32 = 0 + var backdropCount: Int32 = 0 + var symbolCount: Int32 = 0 for attribute in filterAttributes { switch attribute { @@ -988,25 +957,13 @@ final class GiftStoreScreenComponent: Component { } if modelCount > 0 { - if modelCount > 1 { - modelTitle = "\(modelCount) Models" - } else { - modelTitle = "1 Model" - } + modelTitle = environment.strings.Gift_Store_Filter_Selected_Model(modelCount) } if backdropCount > 0 { - if backdropCount > 1 { - backdropTitle = "\(backdropCount) Backdrops" - } else { - backdropTitle = "1 Backdrop" - } + backdropTitle = environment.strings.Gift_Store_Filter_Selected_Backdrop(modelCount) } if symbolCount > 0 { - if symbolCount > 1 { - symbolTitle = "\(symbolCount) Symbols" - } else { - symbolTitle = "1 Symbol" - } + symbolTitle = environment.strings.Gift_Store_Filter_Selected_Symbol(modelCount) } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index e4b0550757..3442c6f6f6 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -407,7 +407,6 @@ private final class GiftViewSheetContent: CombinedComponent { let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let mode = ContactSelectionControllerMode.starsGifting(birthdays: nil, hasActions: false, showSelf: true, selfSubtitle: presentationData.strings.Premium_Gift_ContactSelection_BuySelf) - //TODO:localize let controller = self.context.sharedContext.makeContactSelectionController(ContactSelectionControllerParams( context: context, mode: mode, @@ -476,14 +475,13 @@ private final class GiftViewSheetContent: CombinedComponent { 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), + content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_SuccessYou_Title, text: presentationData.strings.Gift_View_Resale_SuccessYou_Text(giftTitle).string, undoText: nil, customAction: nil), elevatedLayout: lastController is ChatController, action: { _ in return true @@ -505,7 +503,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let peer, let lastController = navigationController?.viewControllers.last as? ViewController, let animationFile { let resultController = UndoOverlayController( presentationData: presentationData, - content: .sticker(context: context, file: animationFile, loop: false, title: "Gift Sent", text: "\(peer.compactDisplayTitle) has been notified about your gift.", undoText: nil, customAction: nil), + content: .sticker(context: context, file: animationFile, loop: false, title: presentationData.strings.Gift_View_Resale_Success_Title, text: presentationData.strings.Gift_View_Resale_Success_Text(peer.compactDisplayTitle).string, undoText: nil, customAction: nil), elevatedLayout: lastController is ChatController, action: { _ in return true @@ -1848,12 +1846,11 @@ private final class GiftViewSheetContent: CombinedComponent { ) buttonOriginX += buttonWidth + buttonSpacing - //TODO:localize let resellButton = resellButton.update( component: PlainButtonComponent( content: AnyComponent( HeaderButtonComponent( - title: uniqueGift.resellStars == nil ? "sell" : "unlist", + title: uniqueGift.resellStars == nil ? strings.Gift_View_Sell : strings.Gift_View_Unlist, iconName: uniqueGift.resellStars == nil ? "Premium/Collectible/Sell" : "Premium/Collectible/Unlist" ) ), @@ -2250,7 +2247,7 @@ private final class GiftViewSheetContent: CombinedComponent { ) } if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { - } else { + } else if ownerPeerId != component.context.account.peerId { selling = true } } @@ -2264,14 +2261,13 @@ private final class GiftViewSheetContent: CombinedComponent { var addressToOpen: String? var descriptionText: String if let uniqueGift, selling { - //TODO:localize let ownerName: String if case let .peerId(peerId) = uniqueGift.owner { ownerName = state.peerMap[peerId]?.compactDisplayTitle ?? "" } else { ownerName = "" } - descriptionText = "\(ownerName) is selling this gift and you can buy it." + descriptionText = strings.Gift_View_SellingGiftInfo(ownerName).string } else if let uniqueGift, let address = uniqueGift.giftAddress, case .address = uniqueGift.owner { addressToOpen = address descriptionText = strings.Gift_View_TonGiftAddressInfo @@ -2568,8 +2564,7 @@ private final class GiftViewSheetContent: CombinedComponent { if state.cachedStarImage == nil || state.cachedStarImage?.1 !== theme { state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: theme.list.itemCheckColors.foregroundColor)!, theme) } - //TODO:localize - var upgradeString = "Buy for" + var upgradeString = strings.Gift_View_BuyFor upgradeString += " # \(presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))" let buttonTitle = subject.arguments?.upgradeStars != nil ? strings.Gift_Upgrade_Confirm : upgradeString @@ -3412,14 +3407,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))" let reference = arguments.reference ?? .slug(slug: gift.slug) - //TODO:localize if let resellStars = gift.resellStars, resellStars > 0, !update { let alertController = textAlertController( context: context, - title: "Unlist This Item?", - text: "It will no longer be for sale.", + title: presentationData.strings.Gift_View_Resale_Unlist_Title, + text: presentationData.strings.Gift_View_Resale_Unlist_Text, actions: [ - TextAlertAction(type: .defaultAction, title: "Unlist", action: { [weak self] in + TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_View_Resale_Unlist_Unlist, action: { [weak self] in guard let self else { return } @@ -3436,7 +3430,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { break } - let text = "\(giftTitle) is removed from sale." + let text = presentationData.strings.Gift_View_Resale_Unlist_Success(giftTitle).string let tooltipController = UndoOverlayController( presentationData: presentationData, content: .universalImage( @@ -3486,9 +3480,10 @@ public class GiftViewScreen: ViewControllerComponentContainer { break } - var text = "\(giftTitle) is now for sale!" + var text = presentationData.strings.Gift_View_Resale_List_Success(giftTitle).string if update { - text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars." + let starsString = presentationData.strings.Gift_View_Resale_Relist_Success_Stars(Int32(price)) + text = presentationData.strings.Gift_View_Resale_Relist_Success(giftTitle, starsString).string } let tooltipController = UndoOverlayController( @@ -3588,6 +3583,16 @@ public class GiftViewScreen: ViewControllerComponentContainer { }))) } + if case let .unique(gift) = arguments.gift, let resellStars = gift.resellStars, resellStars > 0 { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) + }, action: { c, _ in + c?.dismiss(completion: nil) + + resellGiftImpl?(true) + }))) + } + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_CopyLink, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Link"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -3625,8 +3630,7 @@ 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 + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ViewInProfile, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c?.dismiss(completion: nil) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 2c7db7f507..9dcf4c1f25 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -502,8 +502,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr resellPrice = gift.resellStars if let _ = resellPrice { - //TODO:localize - ribbonText = "sale" + ribbonText = params.presentationData.strings.PeerInfo_Gifts_Sale ribbonFont = .larger ribbonColor = .green ribbonOutline = params.presentationData.theme.list.blocksBackgroundColor diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 0ea0dd5b11..3956966391 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -148,10 +148,9 @@ private final class SheetContent: CombinedComponent { maxAmount = withdrawConfiguration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) } amountLabel = nil case let .starGiftResell(update): - //TODO:localize - titleString = update ? "Edit Price" : "Sell Gift" - amountTitle = "PRICE IN STARS" - amountPlaceholder = "Enter Price" + titleString = update ? environment.strings.Stars_SellGift_EditTitle : environment.strings.Stars_SellGift_Title + amountTitle = environment.strings.Stars_SellGift_AmountTitle + amountPlaceholder = environment.strings.Stars_SellGift_AmountPlaceholder minAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMinAmount, nanos: 0) maxAmount = StarsAmount(value: resaleConfiguration.starGiftResaleMaxAmount, nanos: 0) @@ -269,12 +268,13 @@ private final class SheetContent: CombinedComponent { maximumNumberOfLines: 0 )) case .starGiftResell: - //TODO:localize let amountInfoString: NSAttributedString if let value = state.amount?.value, value > 0 { - amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **\(Int32(floor(Float(value) * 0.8))) Stars**.", attributes: amountMarkdownAttributes, textAlignment: .natural)) + let starsValue = Int32(floor(Float(value) * Float(resaleConfiguration.paidMessageCommissionPermille) / 1000.0)) + let starsString = environment.strings.Stars_SellGift_AmountInfo_Stars(starsValue) + amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(starsString).string, attributes: amountMarkdownAttributes, textAlignment: .natural)) } else { - amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString("You will receive **80%**.", attributes: amountMarkdownAttributes, textAlignment: .natural)) + amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.paidMessageCommissionPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural)) } amountFooter = AnyComponent(MultilineTextComponent( text: .plain(amountInfoString), @@ -334,11 +334,10 @@ private final class SheetContent: CombinedComponent { if case .paidMedia = component.mode { buttonString = environment.strings.Stars_PaidContent_Create } else if case .starGiftResell = component.mode { - //TODO:localize if let amount = state.amount, amount.value > 0 { - buttonString = "Sell for # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" + buttonString = "\(environment.strings.Stars_SellGift_SellFor) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" } else { - buttonString = "Sell" + buttonString = environment.strings.Stars_SellGift_Sell } } else if let amount = state.amount { buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" diff --git a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift index e74039e14e..572a05d9ae 100644 --- a/submodules/Utils/DeviceModel/Sources/DeviceModel.swift +++ b/submodules/Utils/DeviceModel/Sources/DeviceModel.swift @@ -124,6 +124,7 @@ public enum DeviceModel: CaseIterable, Equatable { case iPhone16Plus case iPhone16Pro case iPhone16ProMax + case iPhone16e case unknown(String) @@ -235,6 +236,8 @@ public enum DeviceModel: CaseIterable, Equatable { return ["iPhone17,1"] case .iPhone16ProMax: return ["iPhone17,2"] + case .iPhone16e: + return ["iPhone17,5"] case let .unknown(modelId): return [modelId] } @@ -348,6 +351,8 @@ public enum DeviceModel: CaseIterable, Equatable { return "iPhone 16 Pro" case .iPhone16ProMax: return "iPhone 16 Pro Max" + case .iPhone16e: + return "iPhone 16e" case let .unknown(modelId): if modelId.hasPrefix("iPhone") { return "Unknown iPhone" diff --git a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift index e64a909355..d3245faa72 100644 --- a/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift +++ b/submodules/WebUI/Sources/WebAppSecureStorageTransferScreen.swift @@ -103,7 +103,6 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0)) ) - //TODO:localize let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString(string: strings.WebApp_ImportData_Title, font: titleFont, textColor: textColor)), From 1ed853e255c86310245a48ae9d826f8d6131f7eb Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 25 Apr 2025 14:33:25 +0400 Subject: [PATCH 09/10] Various fixes --- .../Navigation/NavigationModalContainer.swift | 2 +- .../Sources/ItemListStickerPackItem.swift | 4 ++-- .../InstalledStickerPacksController.swift | 2 +- .../TelegramEngine/Payments/Stars.swift | 4 ++++ .../Sources/QuickShareToastScreen.swift | 18 ++++++++--------- .../Sources/GiftViewScreen.swift | 7 ++----- .../Sources/MediaEditorScreen.swift | 20 ++++++++++++------- .../Sources/SharedAccountContext.swift | 4 ++-- 8 files changed, 34 insertions(+), 27 deletions(-) diff --git a/submodules/Display/Source/Navigation/NavigationModalContainer.swift b/submodules/Display/Source/Navigation/NavigationModalContainer.swift index 3a86497e78..7b1b433995 100644 --- a/submodules/Display/Source/Navigation/NavigationModalContainer.swift +++ b/submodules/Display/Source/Navigation/NavigationModalContainer.swift @@ -283,7 +283,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes let transition: ContainedViewLayoutTransition let dismissProgress: CGFloat if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() { - if let controller = self.container.controllers.last as? MinimizableController { + if let controller = self.container.controllers.last as? MinimizableController, controller.isMinimizable { dismissProgress = 0.0 targetOffset = 0.0 transition = .immediate diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 7fd1c4f3b4..4833ed62b3 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -370,7 +370,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { } let packRevealOptions: [ItemListRevealOption] - if item.editing.editable && item.enabled { + if item.editing.editable && item.enabled && !item.editing.editing { packRevealOptions = [ItemListRevealOption(key: 0, title: item.presentationData.strings.Common_Delete, icon: .none, color: item.presentationData.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.presentationData.theme.list.itemDisclosureActions.destructive.foregroundColor)] } else { packRevealOptions = [] @@ -564,7 +564,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor } - let revealOffset = strongSelf.revealOffset + let revealOffset = !packRevealOptions.isEmpty ? strongSelf.revealOffset : 0.0 let transition: ContainedViewLayoutTransition if animated { diff --git a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift index e231483dcb..f737897e8f 100644 --- a/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift +++ b/submodules/SettingsUI/Sources/Stickers/InstalledStickerPacksController.swift @@ -1062,7 +1062,7 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { - $0.withUpdatedEditing(true).withUpdatedSelectedPackIds(Set()) + $0.withUpdatedEditing(true).withUpdatedPackIdWithRevealedOptions(nil).withUpdatedSelectedPackIds(Set()) } }) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 1027f65318..464f5f757d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -653,6 +653,9 @@ private extension StarsContext.State.Transaction { if (apiFlags & (1 << 21)) != 0 { flags.insert(.isBusinessTransfer) } + if (apiFlags & (1 << 22)) != 0 { + flags.insert(.isStarGiftResale) + } let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] let _ = subscriptionPeriod @@ -706,6 +709,7 @@ public final class StarsContext { public static let isStarGiftUpgrade = Flags(rawValue: 1 << 6) public static let isPaidMessage = Flags(rawValue: 1 << 7) public static let isBusinessTransfer = Flags(rawValue: 1 << 8) + public static let isStarGiftResale = Flags(rawValue: 1 << 9) } public enum Peer: Equatable { diff --git a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift index e1c71a1a22..723007aa78 100644 --- a/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift +++ b/submodules/TelegramUI/Components/Chat/QuickShareScreen/Sources/QuickShareToastScreen.swift @@ -101,7 +101,7 @@ private final class QuickShareToastScreenComponent: Component { } func animateIn() { - guard let component = self.component else { + guard let component = self.component, let environment = self.environment else { return } func generateAvatarParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat) -> [CGPoint] { @@ -153,7 +153,7 @@ private final class QuickShareToastScreenComponent: Component { playIconAnimation(0.2) } - let offset = self.bounds.height - self.backgroundView.frame.minY + let offset = self.bounds.height - environment.inputHeight - self.backgroundView.frame.minY self.backgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: 0.35, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in if component.peer.id != component.context.account.peerId { playIconAnimation(0.1) @@ -203,12 +203,8 @@ private final class QuickShareToastScreenComponent: Component { let contentInsets = UIEdgeInsets(top: 10.0, left: 12.0, bottom: 10.0, right: 10.0) - let tabBarHeight: CGFloat - if !environment.safeInsets.left.isZero { - tabBarHeight = 34.0 + environment.safeInsets.bottom - } else { - tabBarHeight = 49.0 + environment.safeInsets.bottom - } + let tabBarHeight = 49.0 + max(environment.safeInsets.bottom, environment.inputHeight) + let containerInsets = UIEdgeInsets( top: environment.safeInsets.top, left: environment.safeInsets.left + 12.0, @@ -394,8 +390,12 @@ public final class QuickShareToastScreen: ViewControllerComponentContainer { super.dismiss() } + private var didCommit = false public func dismissWithCommitAction() { - self.action(.commit) + if !self.didCommit { + self.didCommit = true + self.action(.commit) + } self.dismiss() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 3442c6f6f6..e5c8ac9a48 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -470,10 +470,8 @@ private final class GiftViewSheetContent: CombinedComponent { } if let navigationController = controller.navigationController as? NavigationController { - if recipientPeerId == self.context.account.peerId { - var controllers = navigationController.viewControllers - controllers = controllers.filter({ !($0 is GiftViewScreen) }) - navigationController.setViewControllers(controllers, animated: true) + if recipientPeerId == self.context.account.peerId { + controller.dismissAnimated() navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds)) @@ -516,7 +514,6 @@ private final class GiftViewSheetContent: CombinedComponent { } } - controller.animateSuccess() self.updated(transition: .spring(duration: 0.4)) Queue.mainQueue().after(0.5) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index bf319c7e24..eb21ed4c3b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -7386,7 +7386,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return true } - private func processMultipleItems(items: [EditingItem]) { + private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) { guard !items.isEmpty else { return } @@ -7428,7 +7428,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID order.append(randomId) if item.asset.mediaType == .video { - processVideoItem(item: item, index: index, randomId: randomId) { result in + processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in let _ = multipleResults.modify { results in var updatedResults = results updatedResults.append(result) @@ -7473,7 +7473,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } - private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) { + private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) { let asset = item.asset let itemMediaEditor = setupMediaEditorForItem(item: item) @@ -7494,10 +7494,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } let firstFrameTime: CMTime - if let coverImageTimestamp = item.values?.coverImageTimestamp { + if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 { firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) } else { - firstFrameTime = .zero + firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) } PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in @@ -7641,11 +7641,15 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor { + var values = item.values + if values?.videoTrimRange == nil { + values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration) + } return MediaEditor( context: self.context, mode: .default, subject: .asset(item.asset), - values: item.values, + values: values, hasHistogram: false, isStandalone: true ) @@ -8160,6 +8164,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } var multipleItems: [EditingItem] = [] + var isLongVideo = false if self.node.items.count > 1 { multipleItems = self.node.items.filter({ $0.isEnabled }) } else if case let .asset(asset) = self.node.subject { @@ -8187,11 +8192,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID start += storyMaxVideoDuration } + isLongVideo = true } } if multipleItems.count > 1 { - self.processMultipleItems(items: multipleItems) + self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo) } else { self.processSingleItem() } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 474a9c21be..1d0d05c501 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2991,8 +2991,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { let _ = (combineLatest( queue: Queue.mainQueue(), controller.result, - options.get()) - |> take(1)).startStandalone(next: { [weak controller] result, options in + options.get() |> distinctUntilChanged + )).startStandalone(next: { [weak controller] result, options in if let (peers, _, _, _, _, _) = result, let contactPeer = peers.first, case let .peer(peer, _, _) = contactPeer, let starsContext = context.starsContext { if case .starGiftTransfer = source { presentTransferAlertImpl?(EnginePeer(peer)) From 9e0600edfa45ce017d315be9101615dc480717d8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 25 Apr 2025 18:13:30 +0400 Subject: [PATCH 10/10] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/MediaPickerScreen.swift | 6 +- .../CameraScreen/Sources/CameraScreen.swift | 21 +- .../Sources/GiftStoreScreen.swift | 2 - .../Sources/GiftViewScreen.swift | 30 +- .../MediaEditor/Sources/MediaEditor.swift | 2 + .../Sources/EditStories.swift | 2 +- .../Sources/MediaEditorScreen.swift | 862 +----------------- .../Sources/MediaEditorStoryCompletion.swift | 837 +++++++++++++++++ .../Sources/StarsTransactionScreen.swift | 6 +- .../PriceTag.imageset/Contents.json | 12 + .../PriceTag.imageset/price (2).pdf | Bin 0 -> 5743 bytes .../Sources/ChatHistoryListNode.swift | 8 +- .../Sources/ChatTranslationPanelNode.swift | 28 +- .../Sources/SharedAccountContext.swift | 4 +- .../Sources/TelegramRootController.swift | 4 +- 16 files changed, 948 insertions(+), 879 deletions(-) create mode 100644 submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index e8b14c3c91..9d693b6bb7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14290,3 +14290,6 @@ Sorry for the inconvenience."; "MediaPicker.CreateStory_1" = "Create %@ Story"; "MediaPicker.CreateStory_any" = "Create %@ Stories"; "MediaPicker.CombineIntoCollage" = "Combine into Collage"; + +"Gift.Resale.Unavailable.Title" = "Resell Gift"; +"Gift.Resale.Unavailable.Text" = "Sorry, you can't list this gift yet.\n\Reselling will be available on %@."; diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 03575c9fc2..3b9bcc6b5e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -2004,7 +2004,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att var hasSelect = false if forCollage { hasSelect = true - } else if case .story = mode { + } else if case .story = mode, selectionContext.selectionLimit > 1 { hasSelect = true } @@ -3402,7 +3402,7 @@ public func stickerMediaPickerController( destinationCornerRadius: 0.0 ) }, - completion: { result, _, commit in + completion: { result, _, _, commit in completion(result, nil, .zero, nil, true, { _ in return nil }, { returnToCameraImpl?() }) @@ -3520,7 +3520,7 @@ public func avatarMediaPickerController( destinationCornerRadius: 0.0 ) }, - completion: { result, _, commit in + completion: { result, _, _, commit in completion(result, nil, .zero, nil, true, { _ in return nil }, { returnToCameraImpl?() }) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 3bd97fb244..bda8dd68ee 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1959,6 +1959,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { } }, nil, + 1, {} ) } else { @@ -1995,6 +1996,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { } }, nil, + self.controller?.remainingStoryCount, {} ) } @@ -3374,7 +3376,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { self.transitionOut = transitionOut } } - fileprivate let completion: (Signal, ResultTransition?, @escaping () -> Void) -> Void + fileprivate let completion: (Signal, ResultTransition?, Int32?, @escaping () -> Void) -> Void public var transitionedIn: () -> Void = {} public var transitionedOut: () -> Void = {} @@ -3382,6 +3384,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { private let postingAvailabilityPromise = Promise() private var postingAvailabilityDisposable: Disposable? + private var remainingStoryCount: Int32? private var codeDisposable: Disposable? private var resolveCodeDisposable: Disposable? @@ -3419,7 +3422,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { holder: CameraHolder? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, - completion: @escaping (Signal, ResultTransition?, @escaping () -> Void) -> Void + completion: @escaping (Signal, ResultTransition?, Int32?, @escaping () -> Void) -> Void ) { self.context = context self.mode = mode @@ -3473,7 +3476,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { return } if case let .available(remainingCount) = availability { - let _ = remainingCount + self.remainingStoryCount = remainingCount return } self.node.postingAvailable = false @@ -3639,7 +3642,11 @@ public class CameraScreenImpl: ViewController, CameraScreen { if self.cameraState.isCollageEnabled { selectionLimit = 6 } else { - selectionLimit = 10 + if let remainingStoryCount = self.remainingStoryCount { + selectionLimit = min(Int(remainingStoryCount), 10) + } else { + selectionLimit = 10 + } } } controller = self.context.sharedContext.makeStoryMediaPickerScreen( @@ -3704,10 +3711,10 @@ public class CameraScreenImpl: ViewController, CameraScreen { ) self.present(alertController, in: .window(.root)) } else { - self.completion(.single(.asset(asset)), resultTransition, dismissed) + self.completion(.single(.asset(asset)), resultTransition, self.remainingStoryCount, dismissed) } } else if let draft = result as? MediaEditorDraft { - self.completion(.single(.draft(draft)), resultTransition, dismissed) + self.completion(.single(.draft(draft)), resultTransition, self.remainingStoryCount, dismissed) } } } @@ -3753,7 +3760,7 @@ public class CameraScreenImpl: ViewController, CameraScreen { } } else { if let assets = results as? [PHAsset] { - self.completion(.single(.assets(assets)), nil, { + self.completion(.single(.assets(assets)), nil, self.remainingStoryCount, { }) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 8ac35acf59..8dc7594761 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -338,8 +338,6 @@ final class GiftStoreScreenComponent: Component { ) 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 diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index e5c8ac9a48..b0ced0d654 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -2820,7 +2820,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { case upgradePreview([StarGift.UniqueGift.Attribute], String) case wearPreview(StarGift.UniqueGift) - var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?)? { + var arguments: (peerId: EnginePeer.Id?, fromPeerId: EnginePeer.Id?, fromPeerName: String?, messageId: EngineMessage.Id?, reference: StarGiftReference?, incoming: Bool, gift: StarGift, date: Int32, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, pinnedToTop: Bool?, converted: Bool, upgraded: Bool, refunded: Bool, canUpgrade: Bool, upgradeStars: Int64?, transferStars: Int64?, resellStars: Int64?, canExportDate: Int32?, upgradeMessageId: Int32?, canTransferDate: Int32?, canResaleDate: Int32?)? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { @@ -2832,8 +2832,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { } else { reference = .message(messageId: message.id) } - return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId) - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, _, _): + return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil) + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate): var reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -2857,13 +2857,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { if case let .unique(uniqueGift) = gift { resellStars = uniqueGift.resellStars } - return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil) + return (message.id.peerId, senderId ?? message.author?.id, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellStars, canExportDate, nil, canTransferDate, canResaleDate) default: return nil } } case let .uniqueGift(gift, _), let .wearPreview(gift): - return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil) + return (nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellStars, nil, nil, nil, nil) case let .profileGift(peerId, gift): var messageId: EngineMessage.Id? if case let .message(messageIdValue) = gift.reference { @@ -2873,7 +2873,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { if case let .unique(uniqueGift) = gift.gift { resellStars = uniqueGift.resellStars } - return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellStars, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate) case .soldOutGift: return nil case .upgradePreview: @@ -3400,6 +3400,22 @@ public class GiftViewScreen: ViewControllerComponentContainer { self.dismissAllTooltips() + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if let canResaleDate = arguments.canResaleDate, currentTime < canResaleDate { + let dateString = stringForFullDate(timestamp: canResaleDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat) + let controller = textAlertController( + context: self.context, + title: presentationData.strings.Gift_Resale_Unavailable_Title, + text: presentationData.strings.Gift_Resale_Unavailable_Text(dateString).string, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + parseMarkdown: true + ) + self.present(controller, in: .window(.root)) + return + } + let presentationData = context.sharedContext.currentPresentationData.with { $0 } let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))" let reference = arguments.reference ?? .slug(slug: gift.slug) @@ -3582,7 +3598,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { if case let .unique(gift) = arguments.gift, let resellStars = gift.resellStars, resellStars > 0 { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_ChangePrice, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/PriceTag"), color: theme.contextMenu.primaryColor) }, action: { c, _ in c?.dismiss(completion: nil) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 95104c8142..1f77d01911 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -988,6 +988,8 @@ public final class MediaEditor { if let trimRange = self.values.videoTrimRange { player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) // additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) + } else if let duration = player.currentItem?.duration.seconds, duration > self.maxDuration { + player.currentItem?.forwardPlaybackEndTime = CMTime(seconds: self.maxDuration, preferredTimescale: CMTimeScale(1000)) } if let initialSeekPosition = self.initialSeekPosition { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift index 41904b054d..0b3e30a019 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/EditStories.swift @@ -97,7 +97,7 @@ public extension MediaEditorScreenImpl { var updateProgressImpl: ((Float) -> Void)? let controller = MediaEditorScreenImpl( context: context, - mode: .storyEditor, + mode: .storyEditor(remainingCount: 1), subject: subject, isEditing: !repost, isEditingCover: cover, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index eb21ed4c3b..3cee6aa8cd 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2019,13 +2019,17 @@ final class MediaEditorScreenComponent: Component { } else { minDuration = 1.0 if case .avatarEditor = controller.mode { - maxDuration = 9.9 + maxDuration = avatarMaxVideoDuration } else { if controller.node.items.count > 0 { maxDuration = storyMaxVideoDuration } else { - maxDuration = storyMaxCombinedVideoDuration - segmentDuration = storyMaxVideoDuration + if case let .storyEditor(remainingCount) = controller.mode, remainingCount > 1 { + maxDuration = min(storyMaxCombinedVideoDuration, Double(remainingCount) * storyMaxVideoDuration) + segmentDuration = storyMaxVideoDuration + } else { + maxDuration = storyMaxVideoDuration + } } } } @@ -2843,6 +2847,8 @@ let storyMaxVideoDuration: Double = 60.0 let storyMaxCombinedVideoCount: Int = 3 let storyMaxCombinedVideoDuration: Double = storyMaxVideoDuration * Double(storyMaxCombinedVideoCount) +let avatarMaxVideoDuration: Double = 10.0 + public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UIDropInteractionDelegate { public enum Mode { public enum StickerEditorMode { @@ -2852,7 +2858,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID case businessIntro } - case storyEditor + case storyEditor(remainingCount: Int32) case stickerEditor(mode: StickerEditorMode) case botPreview case avatarEditor @@ -3510,8 +3516,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID values: initialValues, hasHistogram: true ) - if case .storyEditor = controller.mode, self.items.isEmpty { - mediaEditor.maxDuration = storyMaxCombinedVideoDuration + if case let .storyEditor(remainingCount) = controller.mode, self.items.isEmpty { + mediaEditor.maxDuration = min(storyMaxCombinedVideoDuration, Double(remainingCount) * storyMaxVideoDuration) + } else if case .avatarEditor = controller.mode { + mediaEditor.maxDuration = avatarMaxVideoDuration } if case .avatarEditor = controller.mode { @@ -6549,15 +6557,17 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID fileprivate let customTarget: EnginePeer.Id? let forwardSource: (EnginePeer, EngineStoryItem)? - fileprivate let initialCaption: NSAttributedString? - fileprivate let initialPrivacy: EngineStoryPrivacy? - fileprivate let initialMediaAreas: [MediaArea]? - fileprivate let initialVideoPosition: Double? - fileprivate let initialLink: (url: String, name: String?)? + let initialCaption: NSAttributedString? + let initialPrivacy: EngineStoryPrivacy? + let initialMediaAreas: [MediaArea]? + let initialVideoPosition: Double? + let initialLink: (url: String, name: String?)? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool, Bool?) -> TransitionOut? + var didComplete = false + public var cancelled: (Bool) -> Void = { _ in } public var willComplete: (UIImage?, Bool, @escaping () -> Void) -> Void public var completion: ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void @@ -6784,7 +6794,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } - fileprivate var isEmbeddedEditor: Bool { + var isEmbeddedEditor: Bool { return self.isEditingStory || self.isEditingStoryCover || self.forwardSource != nil } @@ -7386,825 +7396,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return true } - private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) { - guard !items.isEmpty else { - return - } - - var items = items - if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) { - var updatedCurrentItem = items[currentItemIndex] - updatedCurrentItem.caption = self.node.getCaption() - updatedCurrentItem.values = mediaEditor.values - items[currentItemIndex] = updatedCurrentItem - } - - let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: []) - let totalItems = 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 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, isLongVideo: isLongVideo) { 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.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 processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, 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) - } - extractStickersFromEntity(entity, into: &stickers) - } - } - - let firstFrameTime: CMTime - if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 { - firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) - } else { - firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) - } - - 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 - } - - let duration: Double - if let videoTrimRange = item.values?.videoTrimRange { - duration = videoTrimRange.upperBound - videoTrimRange.lowerBound - } else { - duration = min(asset.duration, storyMaxVideoDuration) - } - - 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 - - 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) - } - extractStickersFromEntity(entity, into: &stickers) - } - } - - 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 { - var values = item.values - if values?.videoTrimRange == nil { - values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration) - } - return MediaEditor( - context: self.context, - mode: .default, - subject: .asset(item.asset), - values: 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 - } - - var caption = self.node.getCaption() - caption = convertMarkdownToAttributes(caption) - - var hasEntityChanges = false - let randomId: Int64 - if case let .draft(_, id) = actualSubject, let id { - randomId = id - } else { - randomId = Int64.random(in: .min ... .max) - } - - let codableEntities = mediaEditor.values.entities - var mediaAreas: [MediaArea] = [] - if case let .draft(draft, _) = actualSubject { - if draft.values.entities != codableEntities { - hasEntityChanges = true - } - } else { - mediaAreas = self.initialMediaAreas ?? [] - } - - var stickers: [TelegramMediaFile] = [] - for entity in codableEntities { - 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 - } - if let mediaArea = entity.mediaArea { - mediaAreas.append(mediaArea) - } - } - - var hasAnyChanges = self.node.hasAnyChanges - if self.isEditingStoryCover { - hasAnyChanges = false - } - - 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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in - self?.dismiss() - Queue.mainQueue().justDispatch { - finished() - } - }) - }) - return - } - - if !(self.isEditingStory || self.isEditingStoryCover) { - let privacy = self.state.privacy - let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in - if let current { - return current.withUpdatedPrivacy(privacy) - } else { - return MediaEditorStoredState(privacy: privacy, textSettings: nil) - } - }).start() - } - - if mediaEditor.resultIsVideo { - self.saveDraft(id: randomId) - - var firstFrame: Signal<(UIImage?, UIImage?), NoError> - let firstFrameTime: CMTime - if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { - firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) - } else { - firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) - } - let videoResult: Signal - var videoIsMirrored = false - let duration: Double - switch subject { - case let .empty(dimensions): - let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in - context.clear(CGRect(origin: .zero, size: size)) - })! - let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" - if let data = image.jpegData(compressionQuality: 0.85) { - try? data.write(to: URL(fileURLWithPath: tempImagePath)) - } - videoResult = .single(.imageFile(path: tempImagePath)) - duration = 3.0 - - firstFrame = .single((image, nil)) - case let .image(image, _, _, _): - let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" - if let data = image.jpegData(compressionQuality: 0.85) { - try? data.write(to: URL(fileURLWithPath: tempImagePath)) - } - videoResult = .single(.imageFile(path: tempImagePath)) - duration = 5.0 - - firstFrame = .single((image, nil)) - case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _): - videoIsMirrored = mirror - videoResult = .single(.videoFile(path: path)) - if let videoTrimRange = mediaEditor.values.videoTrimRange { - duration = videoTrimRange.upperBound - videoTrimRange.lowerBound - } else { - duration = durationValue - } - - var additionalPath = additionalPath - if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath { - additionalPath = valuesAdditionalPath - } - - firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in - let avAsset = AVURLAsset(url: URL(fileURLWithPath: path)) - let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) - avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in - if let cgImage { - if let additionalPath { - let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) - let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) - avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in - if let additionalCGImage { - subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage))) - subscriber.putCompletion() - } else { - subscriber.putNext((UIImage(cgImage: cgImage), nil)) - subscriber.putCompletion() - } - }) - } else { - subscriber.putNext((UIImage(cgImage: cgImage), nil)) - subscriber.putCompletion() - } - } - }) - return ActionDisposable { - avAssetGenerator.cancelAllCGImageGeneration() - } - } - case let .videoCollage(items): - var maxDurationItem: (Double, Subject.VideoCollageItem)? - for item in items { - switch item.content { - case .image: - break - case let .video(_, duration): - if let (maxDuration, _) = maxDurationItem { - if duration > maxDuration { - maxDurationItem = (duration, item) - } - } else { - maxDurationItem = (duration, item) - } - case let .asset(asset): - if let (maxDuration, _) = maxDurationItem { - if asset.duration > maxDuration { - maxDurationItem = (asset.duration, item) - } - } else { - maxDurationItem = (asset.duration, item) - } - } - } - guard let (maxDuration, mainItem) = maxDurationItem else { - fatalError() - } - switch mainItem.content { - case let .video(path, _): - videoResult = .single(.videoFile(path: path)) - case let .asset(asset): - videoResult = .single(.asset(localIdentifier: asset.localIdentifier)) - default: - fatalError() - } - let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in - context.clear(CGRect(origin: .zero, size: size)) - })! - firstFrame = .single((image, nil)) - if let videoTrimRange = mediaEditor.values.videoTrimRange { - duration = videoTrimRange.upperBound - videoTrimRange.lowerBound - } else { - duration = min(maxDuration, storyMaxVideoDuration) - } - case let .asset(asset): - videoResult = .single(.asset(localIdentifier: asset.localIdentifier)) - if asset.mediaType == .video { - if let videoTrimRange = mediaEditor.values.videoTrimRange { - duration = videoTrimRange.upperBound - videoTrimRange.lowerBound - } else { - duration = min(asset.duration, storyMaxVideoDuration) - } - } else { - duration = 5.0 - } - - var additionalPath: String? - if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath { - additionalPath = valuesAdditionalPath - } - - firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in - if asset.mediaType == .video { - PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in - if let avAsset { - let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) - avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in - if let cgImage { - if let additionalPath { - let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) - let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) - avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in - if let additionalCGImage { - subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage))) - subscriber.putCompletion() - } else { - subscriber.putNext((UIImage(cgImage: cgImage), nil)) - subscriber.putCompletion() - } - }) - } else { - subscriber.putNext((UIImage(cgImage: cgImage), nil)) - subscriber.putCompletion() - } - } - }) - } - } - } else { - let options = PHImageRequestOptions() - options.deliveryMode = .highQualityFormat - PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in - if let image { - if let additionalPath { - let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) - let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) - avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in - if let additionalCGImage { - subscriber.putNext((image, UIImage(cgImage: additionalCGImage))) - subscriber.putCompletion() - } else { - subscriber.putNext((image, nil)) - subscriber.putCompletion() - } - }) - } else { - subscriber.putNext((image, nil)) - subscriber.putCompletion() - } - } - } - } - return EmptyDisposable - } - case let .draft(draft, _): - let draftPath = draft.fullPath(engine: context.engine) - if draft.isVideo { - videoResult = .single(.videoFile(path: draftPath)) - if let videoTrimRange = mediaEditor.values.videoTrimRange { - duration = videoTrimRange.upperBound - videoTrimRange.lowerBound - } else { - duration = min(draft.duration ?? 5.0, storyMaxVideoDuration) - } - firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in - let avAsset = AVURLAsset(url: URL(fileURLWithPath: draftPath)) - let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) - avAssetGenerator.appliesPreferredTrackTransform = true - avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in - if let cgImage { - subscriber.putNext((UIImage(cgImage: cgImage), nil)) - subscriber.putCompletion() - } - }) - return ActionDisposable { - avAssetGenerator.cancelAllCGImageGeneration() - } - } - } else { - videoResult = .single(.imageFile(path: draftPath)) - duration = 5.0 - - if let image = UIImage(contentsOfFile: draftPath) { - firstFrame = .single((image, nil)) - } else { - firstFrame = .single((UIImage(), nil)) - } - } - case .message, .gift: - let peerId: EnginePeer.Id - if case let .message(messageIds) = subject { - peerId = messageIds.first!.peerId - } else { - peerId = self.context.account.peerId - } - - let isNightTheme = mediaEditor.values.nightTheme - let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId) - |> map { _, image, nightImage -> UIImage? in - if isNightTheme { - return nightImage ?? image - } else { - return image - } - } - - videoResult = wallpaper - |> mapToSignal { image in - if let image { - let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" - if let data = image.jpegData(compressionQuality: 0.85) { - try? data.write(to: URL(fileURLWithPath: tempImagePath)) - } - return .single(.imageFile(path: tempImagePath)) - } else { - return .complete() - } - } - - firstFrame = wallpaper - |> map { image in - return (image, nil) - } - duration = 5.0 - case .sticker: - let image = generateImage(storyDimensions, contextGenerator: { size, context in - context.clear(CGRect(origin: .zero, size: size)) - }, opaque: false, scale: 1.0) - let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png" - if let data = image?.pngData() { - try? data.write(to: URL(fileURLWithPath: tempImagePath)) - } - videoResult = .single(.imageFile(path: tempImagePath)) - duration = 3.0 - - firstFrame = .single((image, nil)) - case .assets: - fatalError() - } - - let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult) - .start(next: { [weak self] images, videoResult in - if let self { - let (image, additionalImage) = images - var currentImage = mediaEditor.resultImage - if let image { - mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true) - if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) { - currentImage = updatedImage - } - } - - var inputImage: UIImage - if let currentImage { - inputImage = currentImage - } else if let image { - inputImage = image - } else { - inputImage = UIImage() - } - - makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: mediaEditor.values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in - if let self { - self.willComplete(coverImage, true, { [weak self] in - guard let self else { - 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?.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: 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) - } - }) - } - }) - } - } - - private func updateMediaEditorEntities() { - guard let mediaEditor = self.node.mediaEditor 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) - } - - private var didComplete = false - func requestStoryCompletion(animated: Bool) { - guard let mediaEditor = self.node.mediaEditor, !self.didComplete else { - return - } - - self.didComplete = true - - self.updateMediaEditorEntities() - - mediaEditor.stop() - mediaEditor.invalidate() - self.node.entitiesView.invalidate() - - if let navigationController = self.navigationController as? NavigationController { - navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) - } - - var multipleItems: [EditingItem] = [] - var isLongVideo = false - if self.node.items.count > 1 { - multipleItems = self.node.items.filter({ $0.isEnabled }) - } else if case let .asset(asset) = self.node.subject { - let duration: Double - if let playerDuration = mediaEditor.duration { - duration = playerDuration - } else { - duration = asset.duration - } - if duration > storyMaxVideoDuration { - let originalDuration = mediaEditor.originalDuration ?? asset.duration - let values = mediaEditor.values - - let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration))) - var start = values.videoTrimRange?.lowerBound ?? 0 - for i in 0 ..< storyCount { - let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration)) - - var editingItem = EditingItem(asset: asset) - if i == 0 { - editingItem.caption = self.node.getCaption() - } - editingItem.values = trimmedValues - multipleItems.append(editingItem) - - start += storyMaxVideoDuration - } - isLongVideo = true - } - } - - if multipleItems.count > 1 { - self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo) - } else { - self.processSingleItem() - } - - self.dismissAllTooltips() - } - func requestStickerCompletion(animated: Bool) { guard let mediaEditor = self.node.mediaEditor else { return @@ -8257,13 +7448,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let values = mediaEditor.values.withUpdatedCoverDimensions(dimensions) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: dimensions.aspectFitted(CGSize(width: 1080, height: 1080)), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in if let self, let resultImage { - #if DEBUG - if let data = resultImage.jpegData(compressionQuality: 0.7) { - let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).jpg" - try? data.write(to: URL(fileURLWithPath: path)) - } - #endif - 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() @@ -9105,7 +8289,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.node.updateEditProgress(progress, cancel: cancel) } - fileprivate func dismissAllTooltips() { + func dismissAllTooltips() { self.window?.forEachController({ controller in if let controller = controller as? TooltipScreen { controller.dismiss() diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift new file mode 100644 index 0000000000..9c6d56a8cc --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorStoryCompletion.swift @@ -0,0 +1,837 @@ +import Foundation +import UIKit +import Display +import AVFoundation +import SwiftSignalKit +import TelegramCore +import TextFormat +import Photos +import MediaEditor +import DrawingUI + +extension MediaEditorScreenImpl { + func requestStoryCompletion(animated: Bool) { + guard let mediaEditor = self.node.mediaEditor, !self.didComplete else { + return + } + + self.didComplete = true + + self.updateMediaEditorEntities() + + mediaEditor.stop() + mediaEditor.invalidate() + self.node.entitiesView.invalidate() + + if let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + } + + var multipleItems: [EditingItem] = [] + var isLongVideo = false + if self.node.items.count > 1 { + multipleItems = self.node.items.filter({ $0.isEnabled }) + } else if case let .asset(asset) = self.node.subject { + let duration: Double + if let playerDuration = mediaEditor.duration { + duration = playerDuration + } else { + duration = asset.duration + } + if duration > storyMaxVideoDuration { + let originalDuration = mediaEditor.originalDuration ?? asset.duration + let values = mediaEditor.values + + let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration))) + var start = values.videoTrimRange?.lowerBound ?? 0 + for i in 0 ..< storyCount { + let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration)) + + var editingItem = EditingItem(asset: asset) + if i == 0 { + editingItem.caption = self.node.getCaption() + } + editingItem.values = trimmedValues + multipleItems.append(editingItem) + + start += storyMaxVideoDuration + } + isLongVideo = true + } + } + + if multipleItems.count > 1 { + self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo) + } else { + self.processSingleItem() + } + + self.dismissAllTooltips() + } + + private func processSingleItem() { + guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else { + return + } + + var caption = self.node.getCaption() + caption = convertMarkdownToAttributes(caption) + + var hasEntityChanges = false + let randomId: Int64 + if case let .draft(_, id) = actualSubject, let id { + randomId = id + } else { + randomId = Int64.random(in: .min ... .max) + } + + let codableEntities = mediaEditor.values.entities + var mediaAreas: [MediaArea] = [] + if case let .draft(draft, _) = actualSubject { + if draft.values.entities != codableEntities { + hasEntityChanges = true + } + } else { + mediaAreas = self.initialMediaAreas ?? [] + } + + var stickers: [TelegramMediaFile] = [] + for entity in codableEntities { + 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 + } + if let mediaArea = entity.mediaArea { + mediaAreas.append(mediaArea) + } + } + + var hasAnyChanges = self.node.hasAnyChanges + if self.isEditingStoryCover { + hasAnyChanges = false + } + + 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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) + return + } + + if !(self.isEditingStory || self.isEditingStoryCover) { + let privacy = self.state.privacy + let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in + if let current { + return current.withUpdatedPrivacy(privacy) + } else { + return MediaEditorStoredState(privacy: privacy, textSettings: nil) + } + }).start() + } + + if mediaEditor.resultIsVideo { + self.saveDraft(id: randomId) + + var firstFrame: Signal<(UIImage?, UIImage?), NoError> + let firstFrameTime: CMTime + if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp { + firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) + } else { + firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) + } + let videoResult: Signal + var videoIsMirrored = false + let duration: Double + switch subject { + case let .empty(dimensions): + let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + })! + let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" + if let data = image.jpegData(compressionQuality: 0.85) { + try? data.write(to: URL(fileURLWithPath: tempImagePath)) + } + videoResult = .single(.imageFile(path: tempImagePath)) + duration = 3.0 + + firstFrame = .single((image, nil)) + case let .image(image, _, _, _): + let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" + if let data = image.jpegData(compressionQuality: 0.85) { + try? data.write(to: URL(fileURLWithPath: tempImagePath)) + } + videoResult = .single(.imageFile(path: tempImagePath)) + duration = 5.0 + + firstFrame = .single((image, nil)) + case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _): + videoIsMirrored = mirror + videoResult = .single(.videoFile(path: path)) + if let videoTrimRange = mediaEditor.values.videoTrimRange { + duration = videoTrimRange.upperBound - videoTrimRange.lowerBound + } else { + duration = durationValue + } + + var additionalPath = additionalPath + if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath { + additionalPath = valuesAdditionalPath + } + + firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in + let avAsset = AVURLAsset(url: URL(fileURLWithPath: path)) + let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) + avAssetGenerator.appliesPreferredTrackTransform = true + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in + if let cgImage { + if let additionalPath { + let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) + let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) + avAssetGenerator.appliesPreferredTrackTransform = true + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in + if let additionalCGImage { + subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage))) + subscriber.putCompletion() + } else { + subscriber.putNext((UIImage(cgImage: cgImage), nil)) + subscriber.putCompletion() + } + }) + } else { + subscriber.putNext((UIImage(cgImage: cgImage), nil)) + subscriber.putCompletion() + } + } + }) + return ActionDisposable { + avAssetGenerator.cancelAllCGImageGeneration() + } + } + case let .videoCollage(items): + var maxDurationItem: (Double, Subject.VideoCollageItem)? + for item in items { + switch item.content { + case .image: + break + case let .video(_, duration): + if let (maxDuration, _) = maxDurationItem { + if duration > maxDuration { + maxDurationItem = (duration, item) + } + } else { + maxDurationItem = (duration, item) + } + case let .asset(asset): + if let (maxDuration, _) = maxDurationItem { + if asset.duration > maxDuration { + maxDurationItem = (asset.duration, item) + } + } else { + maxDurationItem = (asset.duration, item) + } + } + } + guard let (maxDuration, mainItem) = maxDurationItem else { + fatalError() + } + switch mainItem.content { + case let .video(path, _): + videoResult = .single(.videoFile(path: path)) + case let .asset(asset): + videoResult = .single(.asset(localIdentifier: asset.localIdentifier)) + default: + fatalError() + } + let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + })! + firstFrame = .single((image, nil)) + if let videoTrimRange = mediaEditor.values.videoTrimRange { + duration = videoTrimRange.upperBound - videoTrimRange.lowerBound + } else { + duration = min(maxDuration, storyMaxVideoDuration) + } + case let .asset(asset): + videoResult = .single(.asset(localIdentifier: asset.localIdentifier)) + if asset.mediaType == .video { + if let videoTrimRange = mediaEditor.values.videoTrimRange { + duration = videoTrimRange.upperBound - videoTrimRange.lowerBound + } else { + duration = min(asset.duration, storyMaxVideoDuration) + } + } else { + duration = 5.0 + } + + var additionalPath: String? + if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath { + additionalPath = valuesAdditionalPath + } + + firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in + if asset.mediaType == .video { + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in + if let avAsset { + let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) + avAssetGenerator.appliesPreferredTrackTransform = true + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in + if let cgImage { + if let additionalPath { + let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) + let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) + avAssetGenerator.appliesPreferredTrackTransform = true + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in + if let additionalCGImage { + subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage))) + subscriber.putCompletion() + } else { + subscriber.putNext((UIImage(cgImage: cgImage), nil)) + subscriber.putCompletion() + } + }) + } else { + subscriber.putNext((UIImage(cgImage: cgImage), nil)) + subscriber.putCompletion() + } + } + }) + } + } + } else { + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in + if let image { + if let additionalPath { + let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath)) + let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) + avAssetGenerator.appliesPreferredTrackTransform = true + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in + if let additionalCGImage { + subscriber.putNext((image, UIImage(cgImage: additionalCGImage))) + subscriber.putCompletion() + } else { + subscriber.putNext((image, nil)) + subscriber.putCompletion() + } + }) + } else { + subscriber.putNext((image, nil)) + subscriber.putCompletion() + } + } + } + } + return EmptyDisposable + } + case let .draft(draft, _): + let draftPath = draft.fullPath(engine: context.engine) + if draft.isVideo { + videoResult = .single(.videoFile(path: draftPath)) + if let videoTrimRange = mediaEditor.values.videoTrimRange { + duration = videoTrimRange.upperBound - videoTrimRange.lowerBound + } else { + duration = min(draft.duration ?? 5.0, storyMaxVideoDuration) + } + firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in + let avAsset = AVURLAsset(url: URL(fileURLWithPath: draftPath)) + let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) + avAssetGenerator.appliesPreferredTrackTransform = true + avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in + if let cgImage { + subscriber.putNext((UIImage(cgImage: cgImage), nil)) + subscriber.putCompletion() + } + }) + return ActionDisposable { + avAssetGenerator.cancelAllCGImageGeneration() + } + } + } else { + videoResult = .single(.imageFile(path: draftPath)) + duration = 5.0 + + if let image = UIImage(contentsOfFile: draftPath) { + firstFrame = .single((image, nil)) + } else { + firstFrame = .single((UIImage(), nil)) + } + } + case .message, .gift: + let peerId: EnginePeer.Id + if case let .message(messageIds) = subject { + peerId = messageIds.first!.peerId + } else { + peerId = self.context.account.peerId + } + + let isNightTheme = mediaEditor.values.nightTheme + let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId) + |> map { _, image, nightImage -> UIImage? in + if isNightTheme { + return nightImage ?? image + } else { + return image + } + } + + videoResult = wallpaper + |> mapToSignal { image in + if let image { + let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" + if let data = image.jpegData(compressionQuality: 0.85) { + try? data.write(to: URL(fileURLWithPath: tempImagePath)) + } + return .single(.imageFile(path: tempImagePath)) + } else { + return .complete() + } + } + + firstFrame = wallpaper + |> map { image in + return (image, nil) + } + duration = 5.0 + case .sticker: + let image = generateImage(storyDimensions, contextGenerator: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + }, opaque: false, scale: 1.0) + let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png" + if let data = image?.pngData() { + try? data.write(to: URL(fileURLWithPath: tempImagePath)) + } + videoResult = .single(.imageFile(path: tempImagePath)) + duration = 3.0 + + firstFrame = .single((image, nil)) + case .assets: + fatalError() + } + + let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult) + .start(next: { [weak self] images, videoResult in + if let self { + let (image, additionalImage) = images + var currentImage = mediaEditor.resultImage + if let image { + mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true) + if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) { + currentImage = updatedImage + } + } + + var inputImage: UIImage + if let currentImage { + inputImage = currentImage + } else if let image { + inputImage = image + } else { + inputImage = UIImage() + } + + var values = mediaEditor.values + if case .avatarEditor = self.mode, values.videoTrimRange == nil && duration > avatarMaxVideoDuration { + values = values.withUpdatedVideoTrimRange(0 ..< avatarMaxVideoDuration) + } + + makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in + if let self { + self.willComplete(coverImage, true, { [weak self] in + guard let self else { + return + } + Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") + self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: 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 { + finished() + } + }) + }) + }) + } + }) + } + }) + + 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) + } + }) + } + }) + } + } + + private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) { + guard !items.isEmpty else { + return + } + + var items = items + if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) { + var updatedCurrentItem = items[currentItemIndex] + updatedCurrentItem.caption = self.node.getCaption() + updatedCurrentItem.values = mediaEditor.values + items[currentItemIndex] = updatedCurrentItem + } + + let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: []) + let totalItems = 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 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, isLongVideo: isLongVideo) { 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.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 processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, 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) + } + extractStickersFromEntity(entity, into: &stickers) + } + } + + let firstFrameTime: CMTime + if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 { + firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) + } else { + firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60)) + } + + 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 + } + + let duration: Double + if let videoTrimRange = item.values?.videoTrimRange { + duration = videoTrimRange.upperBound - videoTrimRange.lowerBound + } else { + duration = min(asset.duration, storyMaxVideoDuration) + } + + 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 + + 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) + } + extractStickersFromEntity(entity, into: &stickers) + } + } + + 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 { + var values = item.values + if values?.videoTrimRange == nil { + values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration) + } + return MediaEditor( + context: self.context, + mode: .default, + subject: .asset(item.asset), + values: 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 + ) + } + + + + func updateMediaEditorEntities() { + guard let mediaEditor = self.node.mediaEditor 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) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 7858518c4d..940c8c781b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -1099,7 +1099,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } if let starRefPeerId = transaction.starrefPeerId, let starRefPeer = state.peerMap[starRefPeerId] { - if !transaction.flags.contains(.isPaidMessage) { + if !transaction.flags.contains(.isPaidMessage) && !transaction.flags.contains(.isStarGiftResale) { tableItems.append(.init( id: "to", title: strings.StarsTransaction_StarRefReason_Affiliate, @@ -1130,7 +1130,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { )) } - if let toPeer { + if let toPeer, !transaction.flags.contains(.isStarGiftResale) { tableItems.append(.init( id: "referred", title: transaction.flags.contains(.isPaidMessage) ? strings.Stars_Transaction_From : strings.StarsTransaction_StarRefReason_Referred, @@ -1162,7 +1162,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } if let starrefCommissionPermille = transaction.starrefCommissionPermille, transaction.starrefPeerId != nil { - if transaction.flags.contains(.isPaidMessage) { + if transaction.flags.contains(.isPaidMessage) || transaction.flags.contains(.isStarGiftResale) { var totalStars = transaction.count if let starrefCount = transaction.starrefAmount { totalStars = totalStars + starrefCount diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json new file mode 100644 index 0000000000..62a7cd1e1d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "price (2).pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/PriceTag.imageset/price (2).pdf new file mode 100644 index 0000000000000000000000000000000000000000..8a4812504c106b49df4fe11d90dac73233a42dc5 GIT binary patch literal 5743 zcmb7Ic_376+c!xxPo;=L>O_(-GiUZu$u3#4?@MD$88Ks-kt`vKLPYkRL?V%7Un@M6 zC}l08MM|MmBHtNX>U*B=d*AcNnR|ZswcpFRu1i8!MNJk&5a4hSKmiO}2RH!i-wz-S zPdQTor0Ee`2P%aLVbFurqSB8u?Ey4IsJ{rQyE0hLD;VbgFU-yJEHD~C@?6PuH)pad zl}{E5_8XdFl){z9+_A|8)|99ib0z#&zTNq8JUKoAKiBIJi79Eukj z#1e=^fQTSqpann!zaYps$TJig3x#fx6@nlWK>~5!HZ&GPBFqN{M}#oKym5FO9*2V> z2$@cT)UhR_K|F}ZtOzQkj6}j?NGl*He*ds0j&OU(?mu_NGU7&|!;it~a5$B2x40*u z5qsam+539o0N~Iddww3pETimWv3LX^|CWz{8jDV0(irsl6N6pcxGD@MGL61+lP>Qr z_J)KWq&}H`ocga6zqYIwr0X~nVE_0)ZxDLZj!_-msDRHuIKj4T!3i{B*$3P*$o4_e zkx6x>lbO)@R-wAnC{%rQrA3?6XzV!vspbext57KnJL=-h(%Hx135$phfPqH8?DR;w z9YfRQ?-p!ioSX`ig~80~tIui2a1@=BO*`Y~n86pxL0-F&uhwdlp8qCQmDQFCYgD&6 zGfnL_Zqn;p!^LH2d1f8_ZzA!6$le0Z9n5#yo!3nYp4a47`1-s{>V1>)BYln|g3hUe za?RqomvPkQh^LC(X3?*!*ER=`TuOy#124FI8cqgGGEQzSstD5Apk5ChbQ(m6aTtmv-iROoPDWAhv0jYl}Ov>I+ zxlG!j+(O@~prRAH=82!+BhLGNFh@U5=QU%sBHscOHp0c~9BR4N0$jlgf?XUXXZGuI zO!>{GZQBOJrES3aVbZvY140Jir7(H`47Kjc(RG!p4F*Kx`~e|k8nA><-B1E{EuuNNp9te zA`9oO6{(Lw@lW}24(NIaY~#&t7#*Qj%0NF}d49C`Q6fabX5xahajGj22M6#b7XIE$_3^9Z~hD5kVNJmAPHOD(o(pkBiTdhgDyOu*1M1PYA}B56!}u1XCS1#xma0g}CXpturh*t7 zNt?aC#y?|buB=k5)Ai4))$6*Vo9?VbK14RZlxv^AH772^G{-}KJmF<2pH8!Gv&H%B z%h#D&Bat0@1XMdSi*hXA;T_h0L9;^7WeK%+6|0!TK1iA)oerC6bQ_bjNET&E-z$p@ zxw33?>WwvwrSwUe=$r|f|LaxZTr*r1f>@ciHlM6n>A4kqZE!ZX^;imDqJ*s;SjcKP z6_|(gh_i~zuT8stk@?c_F1Ay0ONXEwav0HS<8f?|bG>PUp|~lf>OL5jDEv6VJfS*) zmcWfe(sUfI*_WM=aEP{N*=ySy+_t*S)g#ffrP$_C^vL$lcAi32iSk*Q=|kD#?bkX+ zI^)}M9aiaynY}qb4u$75J_&g&-9Flp*zxSCO>T{1jrE*Atj>=Xan!jst#P0#{Svu! za6^M&gNwM4cuBilre|i*{mm4@5Oc_3sA9J%|5c61=ABW!S8rZRrk$ak9g)R!wLaH6 z>fV&swv94okH8k-ci2VP6L%d(xUdEqhw3Vh+UESYk-X82R?CP^pF&n;RKbi4VwZQ3tjVs?W|2>Aw)=%Tm-An*eT|>EJ!$o^cO+>1uTQ%vxQ0u9({y>CiDe{wEgr2VFqk@*g zmVg)&8DBw1(aN2rl7YfSyNB(XDsr3D?z(oXnDDPpnAA$@~{WL_m=P?DL>uB?P*kN^716!GHgitf{L0;2Lgg^`nZvk4w`MBke^jMupEtoX>I^_A; zb8Py?dO492`qu8Zg@q3M#ZvAD&yH0+C`)WC`9>!dMwjAfqYRQU=^lx3=PctyOJ{3; zu#rL)EuAk{R({{G6e)OQ}obFV^ z`?1pgD%B~~psz>2@_b!4#_ILTFfH`38h=}z^&>&^t;BZ;#@B*w-~1;fpPOJWVa+gF zcVyAifNAcp+h#0Nbmesi&Du?`>6KldnBDn2{b$2ikjO>*?NY>vk(`0IReN9LzIJGM z*Fou`#5bmN26ld)X__g#VO&*`Hr3)X=rVGCfWM!$fyI)<=?Hc4Jy$QQZ~#k`CC$Ek6PL=}wmNA)kSFJnGcg z>*N#iLvFf%8u@&@-0c$H&569I2k0R(u^0KPbx!@=orM z+P~K)Z@hH+d~=lAwW`lE!Oxl=WbZr@q+XVZ^lke#J83$G`TJJKo1V7O`%d?JCB1jk zYfQh}zNhxI#5Y$S+&q`#O`Ulcd$515k|Y4)rp!NkP8HM>92ggCZu|7&M6t8qtp2Wu z|N4E$6ZxcXO*37SQZtVyI<$pn#8>3;T=itls730F%EGu**_2TX&ME)U##G?yt+Te(hng<2?rNm$6TOtz z7;1RQ6wm;(#dSJ1_=T3L;@vbA?e2AAUwp|Jrpqf%Dd--vu$@CwoKGbE5nW z4^*uFtl`kqSLM&;Y)iM6rUvI)4QgQse(NBJJtLtU<7V*Jq zVFG#MpM_?dZ&6f)?b~Q=tooyEU+xR8;S^AlYTu+B6oirMdf0yJ#o%C|bt!Ad7P{!Y zMxh5dyqa)c$<_Gh!I|Gl7{gYQ+g*A8wbI+FjDo^S8G_@Inx;ul1TU=8_mb)g&D%e! zq7`U+G+DxBx6p@potGcug-y1)dcAS@VU!%6#YJKiu2)eFdT5b%F`!7l4isa3B#eIz zyaZ)CAEU#Tt z!PZwKMIPU{=4!E`g}#@FeCQ6LXKryRjS~B|$)8E>;OwI%)Jk)v@Tm)C?>aA&9QH*e zHT$qUF_}LhkYLPzr@(F1aFPQx0%m4%ISD=7u}j}6P5EP|-Bru9^^YYzI(lL5v=W(4 zoty80v(%6I&BYcpl~v_gPU5Fq?(%ITdpc@SQeU=MwE*2MVeo(};#gpHp(7ZRn2q0>fGa`tNhTQhcsk7jMK zOLe%BjlVghkTbL?Mb4E7q;$hRk?IXMG_v-K8g_*b$&Jeu)kTfo&!sfNUjGoEd~&Iw zZu>{G9-I8~9UcF0tIi+Cg;JkLz$_nX@b^BRBAw1)x&anTU5ACfo-X$ z+lCr+RUv=%=Ga&CQbSOgL1$9wkVf|9vIOd~`+iISTodRhi5rJ2H;{&g|hzJqBkt)!+-g zBla7B`chE$55gB8cFfriKwpB`O}9TFA_`i9->FCh=o|P4M1n5m-yo>-1$A$JhX|*+HEDMvz^Z^I^qdh&Z@}gz6Es{{h6VyRHBL literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 89a9ef7aed..723f3a53c3 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1946,12 +1946,14 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto } var audioTranscriptionProvidedByBoost = false + var autoTranslate = false var isCopyProtectionEnabled: Bool = data.initialData?.peer?.isCopyProtectionEnabled ?? false for entry in view.additionalData { if case let .peer(_, maybePeer) = entry, let peer = maybePeer { isCopyProtectionEnabled = peer.isCopyProtectionEnabled - if let channel = peer as? TelegramChannel, let boostLevel = channel.approximateBoostLevel { - if boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel { + if let channel = peer as? TelegramChannel { + autoTranslate = channel.flags.contains(.autoTranslateEnabled) + if let boostLevel = channel.approximateBoostLevel, boostLevel >= premiumConfiguration.minGroupAudioTranscriptionLevel { audioTranscriptionProvidedByBoost = true } } @@ -1964,7 +1966,7 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto ) var translateToLanguage: (fromLang: String, toLang: String)? - if let translationState, isPremium && translationState.isEnabled { + if let translationState, (isPremium || autoTranslate) && translationState.isEnabled { var languageCode = translationState.toLang ?? chatPresentationData.strings.baseLanguageCode let rawSuffix = "-raw" if languageCode.hasSuffix(rawSuffix) { diff --git a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift index fb7481b9f0..ae413aabd2 100644 --- a/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTranslationPanelNode.swift @@ -187,19 +187,27 @@ final class ChatTranslationPanelNode: ASDisplayNode { } let isPremium = self.chatInterfaceState?.isPremium ?? false - if isPremium { + + var translationAvailable = isPremium + if let channel = self.chatInterfaceState?.renderedPeer?.chatMainPeer as? TelegramChannel, channel.flags.contains(.autoTranslateEnabled) { + translationAvailable = true + } + + if translationAvailable { self.interfaceInteraction?.toggleTranslation(translationState.isEnabled ? .original : .translated) } else if !translationState.isEnabled { - let context = self.context - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: context, subject: .translation, action: { - let controller = PremiumIntroScreen(context: context, source: .translation) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + if !isPremium { + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .translation, action: { + let controller = PremiumIntroScreen(context: context, source: .translation) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + self.interfaceInteraction?.chatController()?.push(controller) } - self.interfaceInteraction?.chatController()?.push(controller) } } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 1d0d05c501..1ac461a760 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3551,7 +3551,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } let editorController = MediaEditorScreenImpl( context: context, - mode: .storyEditor, + mode: .storyEditor(remainingCount: 1), subject: subject, customTarget: nil, initialCaption: text.flatMap { NSAttributedString(string: $0) }, @@ -3716,7 +3716,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controller = MediaEditorScreenImpl( context: context, - mode: .storyEditor, + mode: .storyEditor(remainingCount: 1), subject: editorSubject, transitionIn: nil, transitionOut: { _, _ in diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 7c6bff3c7c..077ff118fa 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -346,7 +346,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil } }, - completion: { result, resultTransition, dismissed in + completion: { result, resultTransition, storyRemainingCount, dismissed in let subject: Signal = result |> map { value -> MediaEditorScreenImpl.Subject? in func editorPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> MediaEditorScreenImpl.PIPPosition { @@ -422,7 +422,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon let controller = MediaEditorScreenImpl( context: context, - mode: .storyEditor, + mode: .storyEditor(remainingCount: storyRemainingCount ?? 1), subject: subject, customTarget: mediaEditorCustomTarget, transitionIn: transitionIn,