Various improvements

This commit is contained in:
Ilya Laktyushin 2025-04-21 17:02:37 +04:00
parent f696cfb915
commit a8c7b217a4
23 changed files with 978 additions and 457 deletions

View File

@ -173,8 +173,11 @@ private final class CameraContext {
self.positionValue = configuration.position self.positionValue = configuration.position
self._positionPromise = ValuePromise<Camera.Position>(configuration.position) self._positionPromise = ValuePromise<Camera.Position>(configuration.position)
#if targetEnvironment(simulator)
#else
self.setDualCameraEnabled(configuration.isDualEnabled, change: false) self.setDualCameraEnabled(configuration.isDualEnabled, change: false)
#endif
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
selector: #selector(self.sessionRuntimeError), selector: #selector(self.sessionRuntimeError),

View File

@ -67,6 +67,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
public let externalState: ExternalState? public let externalState: ExternalState?
public let animateOut: ActionSlot<Action<()>> public let animateOut: ActionSlot<Action<()>>
public let onPan: () -> Void public let onPan: () -> Void
public let willDismiss: () -> Void
public init( public init(
content: AnyComponent<ChildEnvironmentType>, content: AnyComponent<ChildEnvironmentType>,
@ -76,7 +77,8 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
isScrollEnabled: Bool = true, isScrollEnabled: Bool = true,
externalState: ExternalState? = nil, externalState: ExternalState? = nil,
animateOut: ActionSlot<Action<()>>, animateOut: ActionSlot<Action<()>>,
onPan: @escaping () -> Void = {} onPan: @escaping () -> Void = {},
willDismiss: @escaping () -> Void = {}
) { ) {
self.content = content self.content = content
self.backgroundColor = backgroundColor self.backgroundColor = backgroundColor
@ -86,6 +88,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
self.externalState = externalState self.externalState = externalState
self.animateOut = animateOut self.animateOut = animateOut
self.onPan = onPan self.onPan = onPan
self.willDismiss = willDismiss
} }
public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool { public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool {
@ -222,6 +225,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
let currentContentOffset = scrollView.contentOffset let currentContentOffset = scrollView.contentOffset
targetContentOffset.pointee = currentContentOffset targetContentOffset.pointee = currentContentOffset
if velocity.y > 300.0 { if velocity.y > 300.0 {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: { self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false) self.dismiss?(false)
}) })
@ -233,6 +237,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true) scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true)
} }
} else { } else {
self.component?.willDismiss()
self.animateOut(initialVelocity: initialVelocity, completion: { self.animateOut(initialVelocity: initialVelocity, completion: {
self.dismiss?(false) self.dismiss?(false)
}) })

View File

@ -467,6 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[583071445] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) } dict[583071445] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) }
dict[1484862010] = { return Api.InputReplyTo.parse_inputReplyToStory($0) } dict[1484862010] = { return Api.InputReplyTo.parse_inputReplyToStory($0) }
dict[-251549057] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftChat($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[1764202389] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftUser($0) }
dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) } dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) }
dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) } dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) }

View File

@ -367,6 +367,7 @@ public extension Api {
public extension Api { public extension Api {
indirect enum InputSavedStarGift: TypeConstructorDescription { indirect enum InputSavedStarGift: TypeConstructorDescription {
case inputSavedStarGiftChat(peer: Api.InputPeer, savedId: Int64) case inputSavedStarGiftChat(peer: Api.InputPeer, savedId: Int64)
case inputSavedStarGiftSlug(slug: String)
case inputSavedStarGiftUser(msgId: Int32) case inputSavedStarGiftUser(msgId: Int32)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -378,6 +379,12 @@ public extension Api {
peer.serialize(buffer, true) peer.serialize(buffer, true)
serializeInt64(savedId, buffer: buffer, boxed: false) serializeInt64(savedId, buffer: buffer, boxed: false)
break break
case .inputSavedStarGiftSlug(let slug):
if boxed {
buffer.appendInt32(545636920)
}
serializeString(slug, buffer: buffer, boxed: false)
break
case .inputSavedStarGiftUser(let msgId): case .inputSavedStarGiftUser(let msgId):
if boxed { if boxed {
buffer.appendInt32(1764202389) buffer.appendInt32(1764202389)
@ -391,6 +398,8 @@ public extension Api {
switch self { switch self {
case .inputSavedStarGiftChat(let peer, let savedId): case .inputSavedStarGiftChat(let peer, let savedId):
return ("inputSavedStarGiftChat", [("peer", peer as Any), ("savedId", savedId as Any)]) return ("inputSavedStarGiftChat", [("peer", peer as Any), ("savedId", savedId as Any)])
case .inputSavedStarGiftSlug(let slug):
return ("inputSavedStarGiftSlug", [("slug", slug as Any)])
case .inputSavedStarGiftUser(let msgId): case .inputSavedStarGiftUser(let msgId):
return ("inputSavedStarGiftUser", [("msgId", msgId as Any)]) return ("inputSavedStarGiftUser", [("msgId", msgId as Any)])
} }
@ -412,6 +421,17 @@ public extension Api {
return nil 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? { public static func parse_inputSavedStarGiftUser(_ reader: BufferReader) -> InputSavedStarGift? {
var _1: Int32? var _1: Int32?
_1 = reader.readInt32() _1 = reader.readInt32()

View File

@ -9791,12 +9791,12 @@ public extension Api.functions.payments {
} }
} }
public extension Api.functions.payments { public extension Api.functions.payments {
static func updateStarGiftPrice(slug: String, resellStars: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) { static func updateStarGiftPrice(stargift: Api.InputSavedStarGift, resellStars: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer() let buffer = Buffer()
buffer.appendInt32(-489360582) buffer.appendInt32(1001301217)
serializeString(slug, buffer: buffer, boxed: false) stargift.serialize(buffer, true)
serializeInt64(resellStars, buffer: buffer, boxed: false) 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) let reader = BufferReader(buffer)
var result: Api.Updates? var result: Api.Updates?
if let signature = reader.readInt32() { if let signature = reader.readInt32() {

View File

@ -1509,14 +1509,14 @@ private final class ProfileGiftsContextImpl {
} }
} }
func updateStarGiftResellPrice(slug: String, price: Int64?) { func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) {
self.actionDisposable.set( 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 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 true
} }
return false return false
@ -1529,7 +1529,7 @@ private final class ProfileGiftsContextImpl {
} }
if let index = self.filteredGifts.firstIndex(where: { gift in 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 true
} }
return false 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 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 messageId
case peerId case peerId
case id case id
case slug
} }
case message(messageId: EngineMessage.Id) case message(messageId: EngineMessage.Id)
case peer(peerId: EnginePeer.Id, id: Int64) case peer(peerId: EnginePeer.Id, id: Int64)
case slug(slug: String)
public enum DecodingError: Error { public enum DecodingError: Error {
case generic case generic
@ -2100,6 +2102,8 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
self = .message(messageId: try container.decode(EngineMessage.Id.self, forKey: .messageId)) self = .message(messageId: try container.decode(EngineMessage.Id.self, forKey: .messageId))
case 1: case 1:
self = .peer(peerId: try container.decode(EnginePeer.Id.self, forKey: .peerId), id: try container.decode(Int64.self, forKey: .id)) 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: default:
throw DecodingError.generic throw DecodingError.generic
} }
@ -2116,6 +2120,9 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
try container.encode(1 as Int32, forKey: .type) try container.encode(1 as Int32, forKey: .type)
try container.encode(peerId, forKey: .peerId) try container.encode(peerId, forKey: .peerId)
try container.encode(id, forKey: .id) 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 nil
} }
return .inputSavedStarGiftChat(peer: inputPeer, savedId: id) 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<Never, NoError> { func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal<Never, NoError> {
return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0)) return account.postbox.transaction { transaction in
|> map(Optional.init) return reference.apiStarGiftReference(transaction: transaction)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
} }
|> mapToSignal { updates -> Signal<Void, NoError> in |> mapToSignal { starGift in
if let updates { guard let starGift else {
account.stateManager.addUpdates(updates) return .complete()
} }
return .complete() return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
|> ignoreValues
} }
|> ignoreValues
} }
public extension StarGift.UniqueGift { public extension StarGift.UniqueGift {

View File

@ -153,8 +153,8 @@ public extension TelegramEngine {
return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled) return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled)
} }
public func updateStarGiftResalePrice(slug: String, price: Int64?) -> Signal<Never, NoError> { public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, NoError> {
return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price) return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
} }
} }
} }

View File

@ -866,7 +866,8 @@ public final class GiftItemComponent: Component {
return (TelegramTextAttributes.URL, contents) 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: "#") { 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(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)) labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string))

View File

@ -465,7 +465,6 @@ final class GiftSetupScreenComponent: Component {
self.inProgress = false self.inProgress = false
self.state?.updated() self.state?.updated()
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String? var errorText: String?
switch error { switch error {
case .starGiftOutOfStock: case .starGiftOutOfStock:

View File

@ -72,7 +72,7 @@ final class GiftStoreScreenComponent: Component {
private let loadingNode: LoadingShimmerNode private let loadingNode: LoadingShimmerNode
private let emptyResultsAnimation = ComponentView<Empty>() private let emptyResultsAnimation = ComponentView<Empty>()
private let emptyResultsTitle = ComponentView<Empty>() private let emptyResultsTitle = ComponentView<Empty>()
private let emptyResultsAction = ComponentView<Empty>() private let clearFilters = ComponentView<Empty>()
private let topPanel = ComponentView<Empty>() private let topPanel = ComponentView<Empty>()
private let topSeparator = ComponentView<Empty>() private let topSeparator = ComponentView<Empty>()
@ -139,10 +139,21 @@ final class GiftStoreScreenComponent: Component {
self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate) self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate)
} }
private var removedStarGifts = Set<String>()
private var currentGifts: ([StarGift], Set<String>, Set<String>, Set<String>)? private var currentGifts: ([StarGift], Set<String>, Set<String>, Set<String>)?
private var effectiveGifts: [StarGift]? { private var effectiveGifts: [StarGift]? {
if let gifts = self.state?.starGiftsState?.gifts { 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 { } else {
return nil return nil
} }
@ -154,6 +165,7 @@ final class GiftStoreScreenComponent: Component {
} }
let availableWidth = self.scrollView.bounds.width let availableWidth = self.scrollView.bounds.width
let availableHeight = self.scrollView.bounds.height
let contentOffset = self.scrollView.contentOffset.y let contentOffset = self.scrollView.contentOffset.y
let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0 let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0
@ -213,8 +225,8 @@ final class GiftStoreScreenComponent: Component {
font: .monospaced, font: .monospaced,
color: ribbonColor 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( let _ = visibleItem.update(
transition: itemTransition, transition: itemTransition,
component: AnyComponent( component: AnyComponent(
@ -243,6 +255,13 @@ final class GiftStoreScreenComponent: Component {
context: component.context, context: component.context,
subject: .uniqueGift(uniqueGift, state.peerId) 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) 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) let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
if interactive, bottomContentOffset < 320.0 { if interactive, bottomContentOffset < 320.0 {
self.state?.starGiftsContext.loadMore() self.state?.starGiftsContext.loadMore()
@ -966,118 +1117,7 @@ final class GiftStoreScreenComponent: Component {
loadingTransition.setAlpha(view: self.loadingNode.view, alpha: 0.0) 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)) 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 return availableSize
} }
} }

View File

@ -10,10 +10,10 @@ import AccountContext
import TelegramPresentationData import TelegramPresentationData
final class PriceButtonComponent: Component { final class PriceButtonComponent: Component {
let price: Int64 let price: String
init( init(
price: Int64 price: String
) { ) {
self.price = price self.price = price
} }
@ -54,7 +54,7 @@ final class PriceButtonComponent: Component {
transition: .immediate, transition: .immediate,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString( text: .plain(NSAttributedString(
string: "\(component.price)", string: component.price,
font: Font.semibold(11.0), font: Font.semibold(11.0),
textColor: UIColor(rgb: 0xffffff) textColor: UIColor(rgb: 0xffffff)
)) ))

View File

@ -444,10 +444,24 @@ private final class GiftViewSheetContent: CombinedComponent {
self.updated() self.updated()
self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId) 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 { guard let self, let controller = self.getController() as? GiftViewScreen else {
return return
} }
controller.onBuySuccess()
self.inProgress = false self.inProgress = false
var animationFile: TelegramMediaFile? var animationFile: TelegramMediaFile?
@ -459,41 +473,26 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
if let navigationController = controller.navigationController as? NavigationController { if let navigationController = controller.navigationController as? NavigationController {
if recipientPeerId == self.context.account.peerId { if recipientPeerId == self.context.account.peerId {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) var controllers = navigationController.viewControllers
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in controllers = controllers.filter({ !($0 is GiftViewScreen) })
guard let peer, let navigationController else { navigationController.setViewControllers(controllers, animated: true)
return
//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 { } else {
var controllers = Array(navigationController.viewControllers.prefix(1)) var controllers = Array(navigationController.viewControllers.prefix(1))
@ -884,17 +883,16 @@ private final class GiftViewSheetContent: CombinedComponent {
headerSubject = nil headerSubject = nil
} }
var ownerPeerId: EnginePeer.Id var ownerPeerId: EnginePeer.Id?
if let uniqueGift, case let .peerId(peerId) = uniqueGift.owner { if let uniqueGift, case let .peerId(peerId) = uniqueGift.owner {
ownerPeerId = peerId ownerPeerId = peerId
} else {
ownerPeerId = component.context.account.peerId
} }
let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId
var wearPeerNameChild: _UpdatedChildComponent? var wearPeerNameChild: _UpdatedChildComponent?
if showWearPreview, let uniqueGift { if showWearPreview, let uniqueGift {
var peerName = "" var peerName = ""
if let ownerPeer = state.peerMap[ownerPeerId] { if let ownerPeer = state.peerMap[wearOwnerPeerId] {
peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
} }
wearPeerNameChild = wearPeerName.update( wearPeerNameChild = wearPeerName.update(
@ -1004,7 +1002,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
if let wearPeerNameChild { if let wearPeerNameChild {
if let ownerPeer = state.peerMap[ownerPeerId] { if let ownerPeer = state.peerMap[wearOwnerPeerId] {
let wearAvatar = wearAvatar.update( let wearAvatar = wearAvatar.update(
component: AvatarComponent( component: AvatarComponent(
context: component.context, context: component.context,
@ -1488,8 +1486,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if !soldOut { if !soldOut {
if let uniqueGift { if let uniqueGift {
if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId { if !"".isEmpty, case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
//TODO:localize
if let peer = state.peerMap[recipientPeerId] { if let peer = state.peerMap[recipientPeerId] {
tableItems.append(.init( tableItems.append(.init(
id: "recipient", id: "recipient",
@ -1815,7 +1812,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
let canWear: Bool 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 premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration)) let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
if let boostLevel = channel.approximateBoostLevel { if let boostLevel = channel.approximateBoostLevel {
@ -2232,29 +2229,28 @@ private final class GiftViewSheetContent: CombinedComponent {
if let uniqueGift { if let uniqueGift {
resellStars = uniqueGift.resellStars resellStars = uniqueGift.resellStars
if incoming, let resellStars { if let resellStars {
let priceButton = priceButton.update( if incoming || ownerPeerId == component.context.account.peerId {
component: PlainButtonComponent( let priceButton = priceButton.update(
content: AnyComponent( component: PlainButtonComponent(
PriceButtonComponent(price: resellStars) content: AnyComponent(
PriceButtonComponent(price: presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))
),
effectAlignment: .center,
action: {
component.resellGift(true)
},
animateScale: false
), ),
effectAlignment: .center, availableSize: CGSize(width: 150.0, height: 30.0),
action: { transition: context.transition
component.resellGift(true) )
}, context.add(priceButton
animateScale: false .position(CGPoint(x: environment.safeInsets.left + 16.0 + priceButton.size.width / 2.0, y: 28.0))
), .appear(.default(scale: true, alpha: true))
availableSize: CGSize(width: 120.0, height: 30.0), .disappear(.default(scale: true, alpha: true))
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 {
if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil { if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil {
} else { } else {
selling = true selling = true
@ -2361,7 +2357,7 @@ private final class GiftViewSheetContent: CombinedComponent {
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration)) let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
var canWear = true 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 canWear = false
buttonContent = AnyComponentWithIdentity( buttonContent = AnyComponentWithIdentity(
id: AnyHashable("wear_channel"), id: AnyHashable("wear_channel"),
@ -2421,7 +2417,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if isChannelGift { if isChannelGift {
state.levelsDisposable.set(combineLatest( state.levelsDisposable.set(combineLatest(
queue: Queue.mainQueue(), queue: Queue.mainQueue(),
context.engine.peers.getChannelBoostStatus(peerId: ownerPeerId), context.engine.peers.getChannelBoostStatus(peerId: wearOwnerPeerId),
context.engine.peers.getMyBoostStatus() context.engine.peers.getMyBoostStatus()
).startStandalone(next: { [weak controller] boostStatus, myBoostStatus in ).startStandalone(next: { [weak controller] boostStatus, myBoostStatus in
guard let controller, let boostStatus, let myBoostStatus else { guard let controller, let boostStatus, let myBoostStatus else {
@ -2429,7 +2425,7 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
component.cancel(true) 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) controller.push(levelsController)
HapticFeedback().impact(.light) HapticFeedback().impact(.light)
@ -2763,6 +2759,11 @@ private final class GiftViewSheetComponent: CombinedComponent {
if let controller = controller() as? GiftViewScreen { if let controller = controller() as? GiftViewScreen {
controller.dismissAllTooltips() controller.dismissAllTooltips()
} }
},
willDismiss: {
if let controller = controller() as? GiftViewScreen {
controller.dismissBalanceOverlay()
}
} }
), ),
environment: { environment: {
@ -2901,6 +2902,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
let updateSubject = ActionSlot<GiftViewScreen.Subject>() let updateSubject = ActionSlot<GiftViewScreen.Subject>()
public var disposed: () -> Void = {} public var disposed: () -> Void = {}
public var onBuySuccess: () -> Void = {}
fileprivate var showBalance = false { fileprivate var showBalance = false {
didSet { didSet {
@ -2927,7 +2929,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.context = context self.context = context
self.subject = subject self.subject = subject
var openPeerImpl: ((EnginePeer) -> Void)? var openPeerImpl: ((EnginePeer, Bool) -> Void)?
var openAddressImpl: ((String) -> Void)? var openAddressImpl: ((String) -> Void)?
var copyAddressImpl: ((String) -> Void)? var copyAddressImpl: ((String) -> Void)?
var updateSavedToProfileImpl: ((Bool) -> Void)? var updateSavedToProfileImpl: ((Bool) -> Void)?
@ -2950,7 +2952,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
context: context, context: context,
subject: subject, subject: subject,
openPeer: { peerId in openPeer: { peerId in
openPeerImpl?(peerId) openPeerImpl?(peerId, false)
}, },
openAddress: { address in openAddress: { address in
openAddressImpl?(address) openAddressImpl?(address)
@ -3009,21 +3011,27 @@ public class GiftViewScreen: ViewControllerComponentContainer {
self.navigationPresentation = .flatModal self.navigationPresentation = .flatModal
self.automaticallyControlPresentationContextLayout = false self.automaticallyControlPresentationContextLayout = false
openPeerImpl = { [weak self] peer in openPeerImpl = { [weak self] peer, gifts in
guard let self, let navigationController = self.navigationController as? NavigationController else { guard let self, let navigationController = self.navigationController as? NavigationController else {
return return
} }
self.dismissAllTooltips() self.dismissAllTooltips()
let _ = (context.engine.data.get( if gifts {
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) if let controller = context.sharedContext.makePeerInfoController(
) context: context,
|> deliverOnMainQueue).start(next: { peer in updatedPresentationData: nil,
guard let peer else { peer: peer._asPeer(),
return 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)) 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 } let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
@ -3379,7 +3387,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
guard let peer else { guard let peer else {
return return
} }
openPeerImpl?(peer) openPeerImpl?(peer, false)
Queue.mainQueue().after(0.6) { Queue.mainQueue().after(0.6) {
self?.dismiss(animated: false, completion: nil) self?.dismiss(animated: false, completion: nil)
} }
@ -3397,12 +3405,15 @@ public class GiftViewScreen: ViewControllerComponentContainer {
} }
resellGiftImpl = { [weak self] update in 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 return
} }
self.dismissAllTooltips() self.dismissAllTooltips()
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
//TODO:localize //TODO:localize
if let resellStars = gift.resellStars, resellStars > 0, !update { if let resellStars = gift.resellStars, resellStars > 0, !update {
let alertController = textAlertController( let alertController = textAlertController(
@ -3415,10 +3426,16 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return 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 text = "\(giftTitle) is removed from sale."
let tooltipController = UndoOverlayController( let tooltipController = UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
@ -3442,7 +3459,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let updateResellStars { if let updateResellStars {
updateResellStars(nil) updateResellStars(nil)
} else { } 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() |> deliverOnMainQueue).startStandalone()
} }
}), }),
@ -3458,16 +3476,20 @@ public class GiftViewScreen: ViewControllerComponentContainer {
return return
} }
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price)))) switch self.subject {
case let .profileGift(peerId, currentSubject):
let giftTitle = "\(gift.title) #\(gift.number)" self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
let presentationData = context.sharedContext.currentPresentationData.with { $0 } case let .uniqueGift(_, recipientPeerId):
self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId)
default:
break
}
var text = "\(giftTitle) is now for sale!" var text = "\(giftTitle) is now for sale!"
if update { if update {
text = "\(giftTitle) is relisted for \(price) Stars." text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars."
} }
let tooltipController = UndoOverlayController( let tooltipController = UndoOverlayController(
presentationData: presentationData, presentationData: presentationData,
content: .universalImage( content: .universalImage(
@ -3490,7 +3512,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
if let updateResellStars { if let updateResellStars {
updateResellStars(price) updateResellStars(price)
} else { } 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() |> 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) let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
self.presentInGlobalOverlay(contextController) self.presentInGlobalOverlay(contextController)
}) })

View File

@ -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.context = context
self.mode = mode self.mode = mode
self.subject = subject self.subject = subject
if let values { if let values {
self.values = values self.values = values
self.updateRenderChain() self.updateRenderChain()
@ -581,6 +582,9 @@ public final class MediaEditor {
} }
self.valuesPromise.set(.single(self.values)) self.valuesPromise.set(.single(self.values))
if isStandalone, let device = MTLCreateSystemDefaultDevice() {
self.renderer.setupForStandaloneDevice(device: device)
}
self.renderer.addRenderChain(self.renderChain) self.renderer.addRenderChain(self.renderChain)
if hasHistogram { if hasHistogram {
self.renderer.addRenderPass(self.histogramCalculationPass) self.renderer.addRenderPass(self.histogramCalculationPass)
@ -611,7 +615,7 @@ public final class MediaEditor {
} }
public func replaceSource(_ image: UIImage, additionalImage: UIImage?, time: CMTime, mirror: Bool) { 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 return
} }
let additionalTexture = additionalImage.flatMap { loadTexture(image: $0, device: device) } let additionalTexture = additionalImage.flatMap { loadTexture(image: $0, device: device) }

View File

@ -125,7 +125,7 @@ final class MediaEditorRenderer {
func addRenderPass(_ renderPass: RenderPass) { func addRenderPass(_ renderPass: RenderPass) {
self.renderPasses.append(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) renderPass.setup(device: device, library: library)
} }
} }
@ -160,6 +160,14 @@ final class MediaEditorRenderer {
self.renderPasses.forEach { $0.setup(device: device, library: library) } 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() { private func setup() {
guard let device = self.renderTarget?.mtlDevice else { guard let device = self.renderTarget?.mtlDevice else {
return return
@ -180,6 +188,11 @@ final class MediaEditorRenderer {
self.commonSetup(device: device) self.commonSetup(device: device)
} }
func setupForStandaloneDevice(device: MTLDevice) {
self.device = device
self.commonSetup(device: device)
}
func setRate(_ rate: Float) { func setRate(_ rate: Float) {
self.textureSource?.setRate(rate) self.textureSource?.setRate(rate)
} }
@ -240,15 +253,7 @@ final class MediaEditorRenderer {
} }
func renderFrame() { func renderFrame() {
let device: MTLDevice? guard let device = self.effectiveDevice,
if let renderTarget = self.renderTarget {
device = renderTarget.mtlDevice
} else if let currentDevice = self.device {
device = currentDevice
} else {
device = nil
}
guard let device = device,
let commandQueue = self.commandQueue, let commandQueue = self.commandQueue,
let textureCache = self.textureCache, let textureCache = self.textureCache,
let commandBuffer = commandQueue.makeCommandBuffer(), let commandBuffer = commandQueue.makeCommandBuffer(),
@ -366,7 +371,7 @@ final class MediaEditorRenderer {
} }
func finalRenderedImage(mirror: Bool = false) -> UIImage? { 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) return getTextureImage(device: device, texture: finalTexture, mirror: mirror)
} else { } else {
return nil return nil

View File

@ -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
}
}

View File

@ -122,7 +122,10 @@ public extension MediaEditorScreenImpl {
return transitionOut return transitionOut
} }
}, },
completion: { result, commit in completion: { results, commit in
guard let result = results.first else {
return
}
let entities = generateChatInputTextEntities(result.caption) let entities = generateChatInputTextEntities(result.caption)
if repost { if repost {

View File

@ -338,7 +338,7 @@ final class MediaEditorScreenComponent: Component {
private var isEditingCaption = false private var isEditingCaption = false
private var currentInputMode: MessageInputPanelComponent.InputMode = .text private var currentInputMode: MessageInputPanelComponent.InputMode = .text
private var isSelectionPanelOpen = false fileprivate var isSelectionPanelOpen = false
private var didInitializeInputMediaNodeDataPromise = false private var didInitializeInputMediaNodeDataPromise = false
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
@ -2013,10 +2013,21 @@ final class MediaEditorScreenComponent: Component {
) )
), ),
effectAlignment: .center, effectAlignment: .center,
action: { [weak self] in action: { [weak self, weak controller] in
if let self { if let self, let controller {
self.isSelectionPanelOpen = !self.isSelectionPanelOpen 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() self.state?.updated()
controller.hapticFeedback.impact(.light)
} }
}, },
animateAlpha: false animateAlpha: false
@ -2034,8 +2045,8 @@ final class MediaEditorScreenComponent: Component {
} }
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01) transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01)
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0)
if self.isSelectionPanelOpen { if self.isSelectionPanelOpen {
let selectionPanelFrame = CGRect( let selectionPanelFrame = CGRect(
@ -2061,10 +2072,12 @@ final class MediaEditorScreenComponent: Component {
return return
} }
self.isSelectionPanelOpen = false self.isSelectionPanelOpen = false
self.state?.updated() self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate)
if let id { if let id {
controller.node.switchToItem(id) controller.node.switchToItem(id)
controller.hapticFeedback.impact(.light)
} }
}, },
itemSelectionToggled: { [weak self, weak controller] id in itemSelectionToggled: { [weak self, weak controller] id in
@ -2088,6 +2101,8 @@ final class MediaEditorScreenComponent: Component {
controller.node.items[fromIndex] = toItem controller.node.items[fromIndex] = toItem
controller.node.items[toIndex] = fromItem controller.node.items[toIndex] = fromItem
self.state?.updated(transition: .spring(duration: 0.3)) 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) selectionPanelView.frame = CGRect(origin: .zero, size: availableSize)
} }
} else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { } 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.animateOut(to: buttonView, completion: { [weak selectionPanelView] in
selectionPanelView?.removeFromSuperview() selectionPanelView?.removeFromSuperview()
}) })
@ -4027,7 +4042,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
if gestureRecognizer === self.dismissPanGestureRecognizer { if gestureRecognizer === self.dismissPanGestureRecognizer {
let location = gestureRecognizer.location(in: self.entitiesView) 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 false
} }
return true return true
@ -4188,7 +4203,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
private var previousRotateTimestamp: Double? private var previousRotateTimestamp: Double?
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard !self.isCollageTimelineOpen else { guard !self.isCollageTimelineOpen && !(self.componentHostView?.isSelectionPanelOpen ?? false) else {
return return
} }
if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, !self.entitiesView.hasSelection { 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] var updatedCurrentItem = self.items[currentItemIndex]
updatedCurrentItem.caption = self.getCaption() 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.values = mediaEditor.values
updatedCurrentItem.version += 1 updatedCurrentItem.version += 1
@ -6520,7 +6535,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
public var cancelled: (Bool) -> Void = { _ in } public var cancelled: (Bool) -> Void = { _ in }
public var willComplete: (UIImage?, Bool, @escaping () -> Void) -> Void 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 dismissed: () -> Void = { }
public var willDismiss: () -> Void = { } public var willDismiss: () -> Void = { }
public var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? public var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?
@ -6529,7 +6544,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
private var closeFriends = Promise<[EnginePeer]>() private var closeFriends = Promise<[EnginePeer]>()
private let storiesBlockedPeers: BlockedPeersContext private let storiesBlockedPeers: BlockedPeersContext
private let hapticFeedback = HapticFeedback() fileprivate let hapticFeedback = HapticFeedback()
private var audioSessionDisposable: Disposable? private var audioSessionDisposable: Disposable?
private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>() private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>()
@ -6554,7 +6569,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
transitionIn: TransitionIn?, transitionIn: TransitionIn?,
transitionOut: @escaping (Bool, Bool?) -> TransitionOut?, transitionOut: @escaping (Bool, Bool?) -> TransitionOut?,
willComplete: @escaping (UIImage?, Bool, @escaping () -> Void) -> Void = { _, _, commit in commit() }, 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.context = context
self.mode = mode self.mode = mode
@ -6977,7 +6992,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
let hasPremium = self.context.isPremium let hasPremium = self.context.isPremium
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) 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 currentValue = self.state.privacy.timeout
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
@ -6994,62 +7008,56 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
) )
} }
var items: [ContextMenuItem] = [] let timeoutOptions: [(hours: Int, requiresPremium: Bool)] = [
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) (6, true),
(12, true),
(24, false),
(48, true)
]
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(6), icon: { theme in var items: [ContextMenuItem] = [
if !hasPremium { .action(ContextMenuActionItem(
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor) text: presentationData.strings.Story_Editor_ExpirationText,
} else { textLayout: .multiline,
return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil textFont: .small,
} icon: { _ in return nil },
}, action: { [weak self] _, a in action: emptyAction
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()
}
})))
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) 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)) self.present(contextController, in: .window(.root))
} }
@ -7332,30 +7340,335 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
} }
return true return true
} }
private var didComplete = false private func completeWithMultipleResults(results: [MediaEditorScreenImpl.Result]) {
func requestStoryCompletion(animated: Bool) { // Send all results to completion handler
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else { 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 return
} }
self.didComplete = true 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) }
self.dismissAllTooltips() let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
mediaEditor.stop()
mediaEditor.invalidate() var updatedCurrentItem = self.node.items[currentItemIndex]
self.node.entitiesView.invalidate() updatedCurrentItem.caption = self.node.getCaption()
updatedCurrentItem.values = mediaEditor.values
let context = self.context self.node.items[currentItemIndex] = updatedCurrentItem
if let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
} }
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 entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
var caption = self.node.getCaption() var caption = self.node.getCaption()
caption = convertMarkdownToAttributes(caption) caption = convertMarkdownToAttributes(caption)
@ -7407,7 +7720,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) { if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
self.saveDraft(id: randomId, isEdit: true) 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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss() self?.dismiss()
Queue.mainQueue().justDispatch { Queue.mainQueue().justDispatch {
@ -7737,7 +8050,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
return return
} }
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") 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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss() self?.dismiss()
Queue.mainQueue().justDispatch { Queue.mainQueue().justDispatch {
@ -7754,38 +8067,70 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if case let .draft(draft, id) = actualSubject, id == nil { if case let .draft(draft, id) = actualSubject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false) removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
} }
} else { } else if let image = mediaEditor.resultImage {
if let image = mediaEditor.resultImage { self.saveDraft(id: randomId)
self.saveDraft(id: randomId)
var values = mediaEditor.values
var values = mediaEditor.values var outputDimensions: CGSize?
var outputDimensions: CGSize? if case .avatarEditor = self.mode {
if case .avatarEditor = self.mode { outputDimensions = CGSize(width: 640.0, height: 640.0)
outputDimensions = CGSize(width: 640.0, height: 640.0) values = values.withUpdatedQualityPreset(.profile)
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)
}
})
}
})
} }
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 #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?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss() self?.dismiss()
Queue.mainQueue().justDispatch { Queue.mainQueue().justDispatch {
@ -7955,7 +8300,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
if isVideo { if isVideo {
self.uploadSticker(file, action: .send) self.uploadSticker(file, action: .send)
} else { } else {
self.completion(MediaEditorScreenImpl.Result( self.completion([MediaEditorScreenImpl.Result(
media: .sticker(file: file, emoji: self.effectiveStickerEmoji()), media: .sticker(file: file, emoji: self.effectiveStickerEmoji()),
mediaAreas: [], mediaAreas: [],
caption: NSAttributedString(), 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), options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
stickers: [], stickers: [],
randomId: 0 randomId: 0
), { [weak self] finished in )], { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss() self?.dismiss()
Queue.mainQueue().justDispatch { Queue.mainQueue().justDispatch {
@ -8376,7 +8721,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
result = MediaEditorScreenImpl.Result() 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 self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
guard let self else { guard let self else {
return return

View File

@ -145,23 +145,11 @@ final class SelectionPanelComponent: Component {
selectionLayer.lineWidth = lineWidth selectionLayer.lineWidth = lineWidth
selectionLayer.frame = selectionFrame 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) 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 { } else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil self.selectionLayer = nil
selectionLayer.removeFromSuperlayer() 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) { 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) { 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 { func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {

View File

@ -201,7 +201,10 @@ extension PeerInfoScreenImpl {
commit() commit()
} }
}, },
completion: { [weak self] result, commit in completion: { [weak self] results, commit in
guard let result = results.first else {
return
}
switch result.media { switch result.media {
case let .image(image, _): case let .image(image, _):
resultImage = image resultImage = image
@ -217,7 +220,7 @@ extension PeerInfoScreenImpl {
break break
} }
dismissImpl?() dismissImpl?()
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
) )
editorController.cancelled = { _ in editorController.cancelled = { _ in
cancelled() cancelled()

View File

@ -607,10 +607,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return self.profileGifts.buyStarGift(slug: slug, peerId: peerId) return self.profileGifts.buyStarGift(slug: slug, peerId: peerId)
}, },
updateResellStars: { [weak self] price in updateResellStars: { [weak self] price in
guard let self, case let .unique(uniqueGift) = product.gift else { guard let self, let reference = product.reference else {
return return
} }
self.profileGifts.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price)
}, },
togglePinnedToTop: { [weak self] pinnedToTop in togglePinnedToTop: { [weak self] pinnedToTop in
guard let self else { guard let self else {
@ -1479,6 +1479,8 @@ private extension StarGiftReference {
return "m_\(messageId.id)" return "m_\(messageId.id)"
case let .peer(peerId, id): case let .peer(peerId, id):
return "p_\(peerId.toInt64())_\(id)" return "p_\(peerId.toInt64())_\(id)"
case let .slug(slug):
return "s_\(slug)"
} }
} }
} }

View File

@ -1315,13 +1315,13 @@ extension ChatControllerImpl {
) )
} }
return nil return nil
}, completion: { result, commit in }, completion: { results, commit in
if case let .image(image, _) = result.media { if case let .image(image, _) = results.first?.media {
completion(image) completion(image)
commit({}) commit({})
} }
dismissImpl?() dismissImpl?()
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
) )
editorController.cancelled = { _ in editorController.cancelled = { _ in
cancelled() cancelled()
@ -1930,17 +1930,17 @@ extension ChatControllerImpl {
) )
} }
return nil return nil
}, completion: { [weak self] result, commit in }, completion: { [weak self] results, commit in
dismissImpl?() dismissImpl?()
self?.chatDisplayNode.dismissInput() self?.chatDisplayNode.dismissInput()
Queue.mainQueue().after(0.1) { Queue.mainQueue().after(0.1) {
commit({}) commit({})
if case let .sticker(file, _) = result.media { if case let .sticker(file, _) = results.first?.media {
self?.enqueueStickerFile(file) self?.enqueueStickerFile(file)
} }
} }
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
) )
editorController.cancelled = { _ in editorController.cancelled = { _ in
cancelled() cancelled()

View File

@ -3461,9 +3461,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
) )
} }
return nil return nil
}, completion: { result, commit in }, completion: { results, commit in
completion(result, commit) completion(results.first!, commit)
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
) )
editorController.cancelled = { _ in editorController.cancelled = { _ in
cancelled() cancelled()
@ -3525,13 +3525,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
) )
} }
return nil return nil
}, completion: { result, commit in }, completion: { results, commit in
if case let .sticker(file, emoji) = result.media { if case let .sticker(file, emoji) = results.first?.media {
completion(file, emoji, { completion(file, emoji, {
commit({}) commit({})
}) })
} }
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
) )
editorController.cancelled = { _ in editorController.cancelled = { _ in
cancelled() cancelled()
@ -3558,13 +3558,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
transitionIn: nil, transitionIn: nil,
transitionOut: { finished, isNew in transitionOut: { finished, isNew in
return nil return nil
}, completion: { result, commit in }, completion: { results, commit in
completion(result, commit) completion(results.first!, commit)
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
) )
// editorController.cancelled = { _ in
// cancelled()
// }
return editorController return editorController
} }
@ -3724,7 +3721,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
transitionOut: { _, _ in transitionOut: { _, _ in
return nil 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 targetPeerId: EnginePeer.Id
let target: Stories.PendingTarget let target: Stories.PendingTarget
if let sendAsPeerId = result.options.sendAsPeerId { if let sendAsPeerId = result.options.sendAsPeerId {

View File

@ -444,7 +444,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
} else { } else {
return nil return nil
} }
}, completion: { [weak self] result, commit in }, completion: { [weak self] results, commit in
guard let self else { guard let self else {
dismissCameraImpl?() dismissCameraImpl?()
commit({}) commit({})
@ -453,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
if let customTarget, case .botPreview = customTarget { if let customTarget, case .botPreview = customTarget {
externalState.storyTarget = 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?() dismissCameraImpl?()
return return
@ -464,7 +464,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
target = .peer(id) target = .peer(id)
targetPeerId = id targetPeerId = id
} else { } else {
if let sendAsPeerId = result.options.sendAsPeerId { if let sendAsPeerId = results.first?.options.sendAsPeerId {
target = .peer(sendAsPeerId) target = .peer(sendAsPeerId)
targetPeerId = sendAsPeerId targetPeerId = sendAsPeerId
} else { } else {
@ -486,12 +486,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
externalState.isPeerArchived = channel.storiesHidden ?? false 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?() dismissCameraImpl?()
}) })
} }
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
) )
controller.cancelled = { showDraftTooltip in controller.cancelled = { showDraftTooltip in
if showDraftTooltip { if showDraftTooltip {