mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
f696cfb915
commit
a8c7b217a4
@ -173,7 +173,10 @@ private final class CameraContext {
|
||||
self.positionValue = configuration.position
|
||||
self._positionPromise = ValuePromise<Camera.Position>(configuration.position)
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
#else
|
||||
self.setDualCameraEnabled(configuration.isDualEnabled, change: false)
|
||||
#endif
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
|
@ -67,6 +67,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
public let externalState: ExternalState?
|
||||
public let animateOut: ActionSlot<Action<()>>
|
||||
public let onPan: () -> Void
|
||||
public let willDismiss: () -> Void
|
||||
|
||||
public init(
|
||||
content: AnyComponent<ChildEnvironmentType>,
|
||||
@ -76,7 +77,8 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
isScrollEnabled: Bool = true,
|
||||
externalState: ExternalState? = nil,
|
||||
animateOut: ActionSlot<Action<()>>,
|
||||
onPan: @escaping () -> Void = {}
|
||||
onPan: @escaping () -> Void = {},
|
||||
willDismiss: @escaping () -> Void = {}
|
||||
) {
|
||||
self.content = content
|
||||
self.backgroundColor = backgroundColor
|
||||
@ -86,6 +88,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
self.externalState = externalState
|
||||
self.animateOut = animateOut
|
||||
self.onPan = onPan
|
||||
self.willDismiss = willDismiss
|
||||
}
|
||||
|
||||
public static func ==(lhs: SheetComponent, rhs: SheetComponent) -> Bool {
|
||||
@ -222,6 +225,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
let currentContentOffset = scrollView.contentOffset
|
||||
targetContentOffset.pointee = currentContentOffset
|
||||
if velocity.y > 300.0 {
|
||||
self.component?.willDismiss()
|
||||
self.animateOut(initialVelocity: initialVelocity, completion: {
|
||||
self.dismiss?(false)
|
||||
})
|
||||
@ -233,6 +237,7 @@ public final class SheetComponent<ChildEnvironmentType: Equatable>: Component {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentSize.height - scrollView.contentInset.top), animated: true)
|
||||
}
|
||||
} else {
|
||||
self.component?.willDismiss()
|
||||
self.animateOut(initialVelocity: initialVelocity, completion: {
|
||||
self.dismiss?(false)
|
||||
})
|
||||
|
@ -467,6 +467,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
|
||||
dict[583071445] = { return Api.InputReplyTo.parse_inputReplyToMessage($0) }
|
||||
dict[1484862010] = { return Api.InputReplyTo.parse_inputReplyToStory($0) }
|
||||
dict[-251549057] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftChat($0) }
|
||||
dict[545636920] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftSlug($0) }
|
||||
dict[1764202389] = { return Api.InputSavedStarGift.parse_inputSavedStarGiftUser($0) }
|
||||
dict[1399317950] = { return Api.InputSecureFile.parse_inputSecureFile($0) }
|
||||
dict[859091184] = { return Api.InputSecureFile.parse_inputSecureFileUploaded($0) }
|
||||
|
@ -367,6 +367,7 @@ public extension Api {
|
||||
public extension Api {
|
||||
indirect enum InputSavedStarGift: TypeConstructorDescription {
|
||||
case inputSavedStarGiftChat(peer: Api.InputPeer, savedId: Int64)
|
||||
case inputSavedStarGiftSlug(slug: String)
|
||||
case inputSavedStarGiftUser(msgId: Int32)
|
||||
|
||||
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
|
||||
@ -378,6 +379,12 @@ public extension Api {
|
||||
peer.serialize(buffer, true)
|
||||
serializeInt64(savedId, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .inputSavedStarGiftSlug(let slug):
|
||||
if boxed {
|
||||
buffer.appendInt32(545636920)
|
||||
}
|
||||
serializeString(slug, buffer: buffer, boxed: false)
|
||||
break
|
||||
case .inputSavedStarGiftUser(let msgId):
|
||||
if boxed {
|
||||
buffer.appendInt32(1764202389)
|
||||
@ -391,6 +398,8 @@ public extension Api {
|
||||
switch self {
|
||||
case .inputSavedStarGiftChat(let peer, let savedId):
|
||||
return ("inputSavedStarGiftChat", [("peer", peer as Any), ("savedId", savedId as Any)])
|
||||
case .inputSavedStarGiftSlug(let slug):
|
||||
return ("inputSavedStarGiftSlug", [("slug", slug as Any)])
|
||||
case .inputSavedStarGiftUser(let msgId):
|
||||
return ("inputSavedStarGiftUser", [("msgId", msgId as Any)])
|
||||
}
|
||||
@ -412,6 +421,17 @@ public extension Api {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputSavedStarGiftSlug(_ reader: BufferReader) -> InputSavedStarGift? {
|
||||
var _1: String?
|
||||
_1 = parseString(reader)
|
||||
let _c1 = _1 != nil
|
||||
if _c1 {
|
||||
return Api.InputSavedStarGift.inputSavedStarGiftSlug(slug: _1!)
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
public static func parse_inputSavedStarGiftUser(_ reader: BufferReader) -> InputSavedStarGift? {
|
||||
var _1: Int32?
|
||||
_1 = reader.readInt32()
|
||||
|
@ -9791,12 +9791,12 @@ 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()
|
||||
buffer.appendInt32(-489360582)
|
||||
serializeString(slug, buffer: buffer, boxed: false)
|
||||
buffer.appendInt32(1001301217)
|
||||
stargift.serialize(buffer, true)
|
||||
serializeInt64(resellStars, buffer: buffer, boxed: false)
|
||||
return (FunctionDescription(name: "payments.updateStarGiftPrice", parameters: [("slug", String(describing: slug)), ("resellStars", String(describing: resellStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
return (FunctionDescription(name: "payments.updateStarGiftPrice", parameters: [("stargift", String(describing: stargift)), ("resellStars", String(describing: resellStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
|
||||
let reader = BufferReader(buffer)
|
||||
var result: Api.Updates?
|
||||
if let signature = reader.readInt32() {
|
||||
|
@ -1509,14 +1509,14 @@ private final class ProfileGiftsContextImpl {
|
||||
}
|
||||
}
|
||||
|
||||
func updateStarGiftResellPrice(slug: String, price: Int64?) {
|
||||
func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) {
|
||||
self.actionDisposable.set(
|
||||
_internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price).startStrict()
|
||||
_internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price).startStrict()
|
||||
)
|
||||
|
||||
|
||||
if let index = self.gifts.firstIndex(where: { gift in
|
||||
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
|
||||
if gift.reference == reference {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -1529,7 +1529,7 @@ private final class ProfileGiftsContextImpl {
|
||||
}
|
||||
|
||||
if let index = self.filteredGifts.firstIndex(where: { gift in
|
||||
if case let .unique(uniqueGift) = gift.gift, uniqueGift.slug == slug {
|
||||
if gift.reference == reference {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -1939,9 +1939,9 @@ public final class ProfileGiftsContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateStarGiftResellPrice(slug: String, price: Int64?) {
|
||||
public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) {
|
||||
self.impl.with { impl in
|
||||
impl.updateStarGiftResellPrice(slug: slug, price: price)
|
||||
impl.updateStarGiftResellPrice(reference: reference, price: price)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2082,10 +2082,12 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
|
||||
case messageId
|
||||
case peerId
|
||||
case id
|
||||
case slug
|
||||
}
|
||||
|
||||
case message(messageId: EngineMessage.Id)
|
||||
case peer(peerId: EnginePeer.Id, id: Int64)
|
||||
case slug(slug: String)
|
||||
|
||||
public enum DecodingError: Error {
|
||||
case generic
|
||||
@ -2100,6 +2102,8 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
|
||||
self = .message(messageId: try container.decode(EngineMessage.Id.self, forKey: .messageId))
|
||||
case 1:
|
||||
self = .peer(peerId: try container.decode(EnginePeer.Id.self, forKey: .peerId), id: try container.decode(Int64.self, forKey: .id))
|
||||
case 2:
|
||||
self = .slug(slug: try container.decode(String.self, forKey: .slug))
|
||||
default:
|
||||
throw DecodingError.generic
|
||||
}
|
||||
@ -2116,6 +2120,9 @@ public enum StarGiftReference: Equatable, Hashable, Codable {
|
||||
try container.encode(1 as Int32, forKey: .type)
|
||||
try container.encode(peerId, forKey: .peerId)
|
||||
try container.encode(id, forKey: .id)
|
||||
case let .slug(slug):
|
||||
try container.encode(2 as Int32, forKey: .type)
|
||||
try container.encode(slug, forKey: .slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2130,6 +2137,8 @@ extension StarGiftReference {
|
||||
return nil
|
||||
}
|
||||
return .inputSavedStarGiftChat(peer: inputPeer, savedId: id)
|
||||
case let .slug(slug):
|
||||
return .inputSavedStarGiftSlug(slug: slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2265,8 +2274,15 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer
|
||||
}
|
||||
}
|
||||
|
||||
func _internal_updateStarGiftResalePrice(account: Account, slug: String, price: Int64?) -> Signal<Never, NoError> {
|
||||
return account.network.request(Api.functions.payments.updateStarGiftPrice(slug: slug, resellStars: price ?? 0))
|
||||
func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal<Never, NoError> {
|
||||
return account.postbox.transaction { transaction in
|
||||
return reference.apiStarGiftReference(transaction: transaction)
|
||||
}
|
||||
|> mapToSignal { starGift in
|
||||
guard let starGift else {
|
||||
return .complete()
|
||||
}
|
||||
return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0))
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||
return .single(nil)
|
||||
@ -2279,6 +2295,7 @@ func _internal_updateStarGiftResalePrice(account: Account, slug: String, price:
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
||||
}
|
||||
|
||||
public extension StarGift.UniqueGift {
|
||||
var itemFile: TelegramMediaFile? {
|
||||
|
@ -153,8 +153,8 @@ public extension TelegramEngine {
|
||||
return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled)
|
||||
}
|
||||
|
||||
public func updateStarGiftResalePrice(slug: String, price: Int64?) -> Signal<Never, NoError> {
|
||||
return _internal_updateStarGiftResalePrice(account: self.account, slug: slug, price: price)
|
||||
public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal<Never, NoError> {
|
||||
return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -866,7 +866,8 @@ public final class GiftItemComponent: Component {
|
||||
return (TelegramTextAttributes.URL, contents)
|
||||
}
|
||||
)
|
||||
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(resellPrice)", attributes: attributes))
|
||||
let dateTimeFormat = component.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat
|
||||
let labelText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("#\(presentationStringsFormattedNumber(Int32(resellPrice), dateTimeFormat.groupingSeparator))", attributes: attributes))
|
||||
if let range = labelText.string.range(of: "#") {
|
||||
labelText.addAttribute(NSAttributedString.Key.font, value: Font.semibold(10.0), range: NSRange(range, in: labelText.string))
|
||||
labelText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: NSRange(range, in: labelText.string))
|
||||
|
@ -465,7 +465,6 @@ final class GiftSetupScreenComponent: Component {
|
||||
self.inProgress = false
|
||||
self.state?.updated()
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
var errorText: String?
|
||||
switch error {
|
||||
case .starGiftOutOfStock:
|
||||
|
@ -72,7 +72,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
private let loadingNode: LoadingShimmerNode
|
||||
private let emptyResultsAnimation = ComponentView<Empty>()
|
||||
private let emptyResultsTitle = ComponentView<Empty>()
|
||||
private let emptyResultsAction = ComponentView<Empty>()
|
||||
private let clearFilters = ComponentView<Empty>()
|
||||
|
||||
private let topPanel = ComponentView<Empty>()
|
||||
private let topSeparator = ComponentView<Empty>()
|
||||
@ -139,10 +139,21 @@ final class GiftStoreScreenComponent: Component {
|
||||
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 effectiveGifts: [StarGift]? {
|
||||
if let gifts = self.state?.starGiftsState?.gifts {
|
||||
if !self.removedStarGifts.isEmpty {
|
||||
return gifts.filter { gift in
|
||||
if case let .unique(uniqueGift) = gift {
|
||||
return !self.removedStarGifts.contains(uniqueGift.slug)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return gifts
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -154,6 +165,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
|
||||
let availableWidth = self.scrollView.bounds.width
|
||||
let availableHeight = self.scrollView.bounds.height
|
||||
let contentOffset = self.scrollView.contentOffset.y
|
||||
|
||||
let topPanelAlpha = min(20.0, max(0.0, contentOffset)) / 20.0
|
||||
@ -214,7 +226,7 @@ final class GiftStoreScreenComponent: Component {
|
||||
color: ribbonColor
|
||||
)
|
||||
|
||||
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(uniqueGift.resellStars ?? 0)")
|
||||
let subject: GiftItemComponent.Subject = .uniqueGift(gift: uniqueGift, price: "⭐️\(presentationStringsFormattedNumber(Int32(uniqueGift.resellStars ?? 0), environment.dateTimeFormat.groupingSeparator))")
|
||||
let _ = visibleItem.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(
|
||||
@ -243,6 +255,13 @@ final class GiftStoreScreenComponent: Component {
|
||||
context: component.context,
|
||||
subject: .uniqueGift(uniqueGift, state.peerId)
|
||||
)
|
||||
giftController.onBuySuccess = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.removedStarGifts.insert(uniqueGift.slug)
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
mainController.push(giftController)
|
||||
}
|
||||
}
|
||||
@ -288,6 +307,138 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let fadeTransition = ComponentTransition.easeInOut(duration: 0.25)
|
||||
let emptyResultsActionSize = self.clearFilters.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "Clear Filters", font: Font.regular(17.0), textColor: environment.theme.list.itemAccentColor)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 0
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state?.starGiftsContext.updateFilterAttributes([])
|
||||
},
|
||||
animateScale: false
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableWidth - 44.0 * 2.0, height: 100.0)
|
||||
)
|
||||
|
||||
var showClearFilters = false
|
||||
if let filterAttributes = self.state?.starGiftsState?.filterAttributes, !filterAttributes.isEmpty {
|
||||
showClearFilters = true
|
||||
}
|
||||
|
||||
let topInset: CGFloat = environment.navigationHeight + 39.0
|
||||
let bottomInset: CGFloat = environment.safeInsets.bottom
|
||||
|
||||
var emptyResultsActionFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0),
|
||||
y: max(self.scrollView.contentSize.height - 8.0, availableHeight - bottomInset - emptyResultsActionSize.height - 16.0)
|
||||
),
|
||||
size: emptyResultsActionSize
|
||||
)
|
||||
|
||||
if let effectiveGifts = self.effectiveGifts, effectiveGifts.isEmpty && self.state?.starGiftsState?.dataState != .loading {
|
||||
showClearFilters = true
|
||||
|
||||
let emptyAnimationHeight = 148.0
|
||||
let visibleHeight = availableHeight
|
||||
let emptyAnimationSpacing: CGFloat = 20.0
|
||||
let emptyTextSpacing: CGFloat = 18.0
|
||||
|
||||
let emptyResultsTitleSize = self.emptyResultsTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: "No Matching Gifts", font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)),
|
||||
horizontalAlignment: .center
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableWidth, height: 100.0)
|
||||
)
|
||||
|
||||
let emptyResultsAnimationSize = self.emptyResultsAnimation.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: "ChatListNoResults")
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight)
|
||||
)
|
||||
|
||||
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsActionSize.height + emptyTextSpacing
|
||||
let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0)
|
||||
|
||||
let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize)
|
||||
|
||||
let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize)
|
||||
|
||||
emptyResultsActionFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableWidth - emptyResultsActionSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsActionSize)
|
||||
|
||||
if let view = self.emptyResultsAnimation.view as? LottieComponent.View {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
view.playOnce()
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsAnimationFrame.center)
|
||||
}
|
||||
if let view = self.emptyResultsTitle.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsTitleFrame.center)
|
||||
}
|
||||
} else {
|
||||
if let view = self.emptyResultsAnimation.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
if let view = self.emptyResultsTitle.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if showClearFilters {
|
||||
if let view = self.clearFilters.view {
|
||||
if view.superview == nil {
|
||||
view.alpha = 0.0
|
||||
fadeTransition.setAlpha(view: view, alpha: 1.0)
|
||||
self.insertSubview(view, belowSubview: self.loadingNode.view)
|
||||
}
|
||||
view.bounds = CGRect(origin: .zero, size: emptyResultsActionFrame.size)
|
||||
ComponentTransition.immediate.setPosition(view: view, position: emptyResultsActionFrame.center)
|
||||
|
||||
view.alpha = self.state?.starGiftsState?.attributes.isEmpty == true ? 0.0 : 1.0
|
||||
}
|
||||
} else {
|
||||
if let view = self.clearFilters.view {
|
||||
fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let bottomContentOffset = max(0.0, self.scrollView.contentSize.height - self.scrollView.contentOffset.y - self.scrollView.frame.height)
|
||||
if interactive, bottomContentOffset < 320.0 {
|
||||
self.state?.starGiftsContext.loadMore()
|
||||
@ -967,117 +1118,6 @@ final class GiftStoreScreenComponent: Component {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -10,10 +10,10 @@ import AccountContext
|
||||
import TelegramPresentationData
|
||||
|
||||
final class PriceButtonComponent: Component {
|
||||
let price: Int64
|
||||
let price: String
|
||||
|
||||
init(
|
||||
price: Int64
|
||||
price: String
|
||||
) {
|
||||
self.price = price
|
||||
}
|
||||
@ -54,7 +54,7 @@ final class PriceButtonComponent: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: "\(component.price)",
|
||||
string: component.price,
|
||||
font: Font.semibold(11.0),
|
||||
textColor: UIColor(rgb: 0xffffff)
|
||||
))
|
||||
|
@ -444,10 +444,24 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
self.updated()
|
||||
|
||||
self.buyDisposable = (self.buyGift(uniqueGift.slug, recipientPeerId)
|
||||
|> deliverOnMainQueue).start(completed: { [weak self, weak starsContext] in
|
||||
|> deliverOnMainQueue).start(error: { [weak self] error in
|
||||
guard let self, let controller = self.getController() else {
|
||||
return
|
||||
}
|
||||
|
||||
self.inProgress = false
|
||||
self.updated()
|
||||
|
||||
let errorText = presentationData.strings.Gift_Send_ErrorUnknown
|
||||
|
||||
let alertController = textAlertController(context: context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})], parseMarkdown: true)
|
||||
controller.present(alertController, in: .window(.root))
|
||||
}, completed: { [weak self, weak starsContext] in
|
||||
guard let self, let controller = self.getController() as? GiftViewScreen else {
|
||||
return
|
||||
}
|
||||
controller.onBuySuccess()
|
||||
|
||||
self.inProgress = false
|
||||
|
||||
var animationFile: TelegramMediaFile?
|
||||
@ -460,25 +474,11 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
if let navigationController = controller.navigationController as? NavigationController {
|
||||
if recipientPeerId == self.context.account.peerId {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak navigationController] peer in
|
||||
guard let peer, let navigationController else {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter({ !($0 is GiftViewScreen) })
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
|
||||
//TODO:localize
|
||||
navigationController.view.addSubview(ConfettiView(frame: navigationController.view.bounds))
|
||||
|
||||
Queue.mainQueue().after(0.5, {
|
||||
@ -494,7 +494,6 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
lastController.present(resultController, in: .window(.root))
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
var controllers = Array(navigationController.viewControllers.prefix(1))
|
||||
let chatController = self.context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: recipientPeerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil)
|
||||
@ -884,17 +883,16 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
headerSubject = nil
|
||||
}
|
||||
|
||||
var ownerPeerId: EnginePeer.Id
|
||||
var ownerPeerId: EnginePeer.Id?
|
||||
if let uniqueGift, case let .peerId(peerId) = uniqueGift.owner {
|
||||
ownerPeerId = peerId
|
||||
} else {
|
||||
ownerPeerId = component.context.account.peerId
|
||||
}
|
||||
let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId
|
||||
|
||||
var wearPeerNameChild: _UpdatedChildComponent?
|
||||
if showWearPreview, let uniqueGift {
|
||||
var peerName = ""
|
||||
if let ownerPeer = state.peerMap[ownerPeerId] {
|
||||
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
|
||||
peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
|
||||
}
|
||||
wearPeerNameChild = wearPeerName.update(
|
||||
@ -1004,7 +1002,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
if let wearPeerNameChild {
|
||||
if let ownerPeer = state.peerMap[ownerPeerId] {
|
||||
if let ownerPeer = state.peerMap[wearOwnerPeerId] {
|
||||
let wearAvatar = wearAvatar.update(
|
||||
component: AvatarComponent(
|
||||
context: component.context,
|
||||
@ -1488,8 +1486,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
|
||||
if !soldOut {
|
||||
if let uniqueGift {
|
||||
if case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
|
||||
//TODO:localize
|
||||
if !"".isEmpty, case let .uniqueGift(_, recipientPeerIdValue) = component.subject, let _ = recipientPeerIdValue, let recipientPeerId = state.recipientPeerId {
|
||||
if let peer = state.peerMap[recipientPeerId] {
|
||||
tableItems.append(.init(
|
||||
id: "recipient",
|
||||
@ -1815,7 +1812,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
let canWear: Bool
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId] {
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId] {
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
|
||||
if let boostLevel = channel.approximateBoostLevel {
|
||||
@ -2232,11 +2229,12 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if let uniqueGift {
|
||||
resellStars = uniqueGift.resellStars
|
||||
|
||||
if incoming, let resellStars {
|
||||
if let resellStars {
|
||||
if incoming || ownerPeerId == component.context.account.peerId {
|
||||
let priceButton = priceButton.update(
|
||||
component: PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
PriceButtonComponent(price: resellStars)
|
||||
PriceButtonComponent(price: presentationStringsFormattedNumber(Int32(resellStars), environment.dateTimeFormat.groupingSeparator))
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: {
|
||||
@ -2244,7 +2242,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
},
|
||||
animateScale: false
|
||||
),
|
||||
availableSize: CGSize(width: 120.0, height: 30.0),
|
||||
availableSize: CGSize(width: 150.0, height: 30.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(priceButton
|
||||
@ -2253,8 +2251,6 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
}
|
||||
|
||||
if !incoming, let _ = resellStars {
|
||||
if case let .uniqueGift(_, recipientPeerId) = component.subject, recipientPeerId != nil {
|
||||
} else {
|
||||
selling = true
|
||||
@ -2361,7 +2357,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
let requiredLevel = Int(BoostSubject.wearGift.requiredLevel(group: false, context: component.context, configuration: premiumConfiguration))
|
||||
|
||||
var canWear = true
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[ownerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel {
|
||||
if isChannelGift, case let .channel(channel) = state.peerMap[wearOwnerPeerId], (channel.approximateBoostLevel ?? 0) < requiredLevel {
|
||||
canWear = false
|
||||
buttonContent = AnyComponentWithIdentity(
|
||||
id: AnyHashable("wear_channel"),
|
||||
@ -2421,7 +2417,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
if isChannelGift {
|
||||
state.levelsDisposable.set(combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
context.engine.peers.getChannelBoostStatus(peerId: ownerPeerId),
|
||||
context.engine.peers.getChannelBoostStatus(peerId: wearOwnerPeerId),
|
||||
context.engine.peers.getMyBoostStatus()
|
||||
).startStandalone(next: { [weak controller] boostStatus, myBoostStatus in
|
||||
guard let controller, let boostStatus, let myBoostStatus else {
|
||||
@ -2429,7 +2425,7 @@ private final class GiftViewSheetContent: CombinedComponent {
|
||||
}
|
||||
component.cancel(true)
|
||||
|
||||
let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: ownerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil)
|
||||
let levelsController = context.sharedContext.makePremiumBoostLevelsController(context: context, peerId: wearOwnerPeerId, subject: .wearGift, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: nil)
|
||||
controller.push(levelsController)
|
||||
|
||||
HapticFeedback().impact(.light)
|
||||
@ -2763,6 +2759,11 @@ private final class GiftViewSheetComponent: CombinedComponent {
|
||||
if let controller = controller() as? GiftViewScreen {
|
||||
controller.dismissAllTooltips()
|
||||
}
|
||||
},
|
||||
willDismiss: {
|
||||
if let controller = controller() as? GiftViewScreen {
|
||||
controller.dismissBalanceOverlay()
|
||||
}
|
||||
}
|
||||
),
|
||||
environment: {
|
||||
@ -2901,6 +2902,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
let updateSubject = ActionSlot<GiftViewScreen.Subject>()
|
||||
|
||||
public var disposed: () -> Void = {}
|
||||
public var onBuySuccess: () -> Void = {}
|
||||
|
||||
fileprivate var showBalance = false {
|
||||
didSet {
|
||||
@ -2927,7 +2929,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
|
||||
var openPeerImpl: ((EnginePeer) -> Void)?
|
||||
var openPeerImpl: ((EnginePeer, Bool) -> Void)?
|
||||
var openAddressImpl: ((String) -> Void)?
|
||||
var copyAddressImpl: ((String) -> Void)?
|
||||
var updateSavedToProfileImpl: ((Bool) -> Void)?
|
||||
@ -2950,7 +2952,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
context: context,
|
||||
subject: subject,
|
||||
openPeer: { peerId in
|
||||
openPeerImpl?(peerId)
|
||||
openPeerImpl?(peerId, false)
|
||||
},
|
||||
openAddress: { address in
|
||||
openAddressImpl?(address)
|
||||
@ -3009,21 +3011,27 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
self.navigationPresentation = .flatModal
|
||||
self.automaticallyControlPresentationContextLayout = false
|
||||
|
||||
openPeerImpl = { [weak self] peer in
|
||||
openPeerImpl = { [weak self] peer, gifts in
|
||||
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
||||
return
|
||||
}
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let _ = (context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let peer else {
|
||||
return
|
||||
if gifts {
|
||||
if let controller = context.sharedContext.makePeerInfoController(
|
||||
context: context,
|
||||
updatedPresentationData: nil,
|
||||
peer: peer._asPeer(),
|
||||
mode: .gifts,
|
||||
avatarInitiallyExpanded: false,
|
||||
fromChat: false,
|
||||
requestsContext: nil
|
||||
) {
|
||||
self.push(controller)
|
||||
}
|
||||
} else {
|
||||
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: true, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
@ -3379,7 +3387,7 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
guard let peer else {
|
||||
return
|
||||
}
|
||||
openPeerImpl?(peer)
|
||||
openPeerImpl?(peer, false)
|
||||
Queue.mainQueue().after(0.6) {
|
||||
self?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
@ -3397,12 +3405,15 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
|
||||
resellGiftImpl = { [weak self] update in
|
||||
guard let self, let arguments = self.subject.arguments, case let .profileGift(peerId, currentSubject) = self.subject, case let .unique(gift) = arguments.gift else {
|
||||
guard let self, let arguments = self.subject.arguments, case let .unique(gift) = arguments.gift else {
|
||||
return
|
||||
}
|
||||
|
||||
self.dismissAllTooltips()
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))"
|
||||
|
||||
//TODO:localize
|
||||
if let resellStars = gift.resellStars, resellStars > 0, !update {
|
||||
let alertController = textAlertController(
|
||||
@ -3415,10 +3426,16 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
return
|
||||
}
|
||||
|
||||
switch self.subject {
|
||||
case let .profileGift(peerId, currentSubject):
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil))))
|
||||
case let .uniqueGift(_, recipientPeerId):
|
||||
self.subject = .uniqueGift(gift.withResellStars(nil), recipientPeerId)
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.onBuySuccess()
|
||||
|
||||
let giftTitle = "\(gift.title) #\(gift.number)"
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let text = "\(giftTitle) is removed from sale."
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
@ -3442,7 +3459,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
if let updateResellStars {
|
||||
updateResellStars(nil)
|
||||
} else {
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: nil)
|
||||
let reference = arguments.reference ?? .slug(slug: gift.slug)
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
}),
|
||||
@ -3458,15 +3476,19 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
return
|
||||
}
|
||||
|
||||
switch self.subject {
|
||||
case let .profileGift(peerId, currentSubject):
|
||||
self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price))))
|
||||
|
||||
let giftTitle = "\(gift.title) #\(gift.number)"
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var text = "\(giftTitle) is now for sale!"
|
||||
if update {
|
||||
text = "\(giftTitle) is relisted for \(price) Stars."
|
||||
case let .uniqueGift(_, recipientPeerId):
|
||||
self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var text = "\(giftTitle) is now for sale!"
|
||||
if update {
|
||||
text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars."
|
||||
}
|
||||
|
||||
let tooltipController = UndoOverlayController(
|
||||
presentationData: presentationData,
|
||||
@ -3490,7 +3512,8 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
if let updateResellStars {
|
||||
updateResellStars(price)
|
||||
} else {
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(slug: gift.slug, price: price)
|
||||
let reference = arguments.reference ?? .slug(slug: gift.slug)
|
||||
let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price)
|
||||
|> deliverOnMainQueue).startStandalone()
|
||||
}
|
||||
})
|
||||
@ -3607,6 +3630,28 @@ public class GiftViewScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = arguments.resellStars, case let .uniqueGift(uniqueGift, recipientPeerId) = subject, let _ = recipientPeerId {
|
||||
//TODO:localize
|
||||
items.append(.action(ContextMenuActionItem(text: "View in Profile", icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Peer Info/ShowIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { c, _ in
|
||||
c?.dismiss(completion: nil)
|
||||
|
||||
if case let .peerId(peerId) = uniqueGift.owner {
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
guard let self, let peer else {
|
||||
return
|
||||
}
|
||||
openPeerImpl?(peer, true)
|
||||
Queue.mainQueue().after(0.6) {
|
||||
self.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
let contextController = ContextController(presentationData: presentationData, source: .reference(GiftViewContextReferenceContentSource(controller: self, sourceNode: node)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture)
|
||||
self.presentInGlobalOverlay(contextController)
|
||||
})
|
||||
|
@ -530,10 +530,11 @@ public final class MediaEditor {
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false) {
|
||||
public init(context: AccountContext, mode: Mode, subject: Subject, values: MediaEditorValues? = nil, hasHistogram: Bool = false, isStandalone: Bool = false) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.subject = subject
|
||||
|
||||
if let values {
|
||||
self.values = values
|
||||
self.updateRenderChain()
|
||||
@ -581,6 +582,9 @@ public final class MediaEditor {
|
||||
}
|
||||
self.valuesPromise.set(.single(self.values))
|
||||
|
||||
if isStandalone, let device = MTLCreateSystemDefaultDevice() {
|
||||
self.renderer.setupForStandaloneDevice(device: device)
|
||||
}
|
||||
self.renderer.addRenderChain(self.renderChain)
|
||||
if hasHistogram {
|
||||
self.renderer.addRenderPass(self.histogramCalculationPass)
|
||||
@ -611,7 +615,7 @@ public final class MediaEditor {
|
||||
}
|
||||
|
||||
public func replaceSource(_ image: UIImage, additionalImage: UIImage?, time: CMTime, mirror: Bool) {
|
||||
guard let renderTarget = self.previewView, let device = renderTarget.mtlDevice, let texture = loadTexture(image: image, device: device) else {
|
||||
guard let device = self.renderer.effectiveDevice, let texture = loadTexture(image: image, device: device) else {
|
||||
return
|
||||
}
|
||||
let additionalTexture = additionalImage.flatMap { loadTexture(image: $0, device: device) }
|
||||
|
@ -125,7 +125,7 @@ final class MediaEditorRenderer {
|
||||
|
||||
func addRenderPass(_ renderPass: RenderPass) {
|
||||
self.renderPasses.append(renderPass)
|
||||
if let device = self.renderTarget?.mtlDevice, let library = self.library {
|
||||
if let device = self.effectiveDevice, let library = self.library {
|
||||
renderPass.setup(device: device, library: library)
|
||||
}
|
||||
}
|
||||
@ -160,6 +160,14 @@ final class MediaEditorRenderer {
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
}
|
||||
|
||||
var effectiveDevice: MTLDevice? {
|
||||
if let device = self.renderTarget?.mtlDevice {
|
||||
return device
|
||||
} else {
|
||||
return self.device
|
||||
}
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
guard let device = self.renderTarget?.mtlDevice else {
|
||||
return
|
||||
@ -180,6 +188,11 @@ final class MediaEditorRenderer {
|
||||
self.commonSetup(device: device)
|
||||
}
|
||||
|
||||
func setupForStandaloneDevice(device: MTLDevice) {
|
||||
self.device = device
|
||||
self.commonSetup(device: device)
|
||||
}
|
||||
|
||||
func setRate(_ rate: Float) {
|
||||
self.textureSource?.setRate(rate)
|
||||
}
|
||||
@ -240,15 +253,7 @@ final class MediaEditorRenderer {
|
||||
}
|
||||
|
||||
func renderFrame() {
|
||||
let device: MTLDevice?
|
||||
if let renderTarget = self.renderTarget {
|
||||
device = renderTarget.mtlDevice
|
||||
} else if let currentDevice = self.device {
|
||||
device = currentDevice
|
||||
} else {
|
||||
device = nil
|
||||
}
|
||||
guard let device = device,
|
||||
guard let device = self.effectiveDevice,
|
||||
let commandQueue = self.commandQueue,
|
||||
let textureCache = self.textureCache,
|
||||
let commandBuffer = commandQueue.makeCommandBuffer(),
|
||||
@ -366,7 +371,7 @@ final class MediaEditorRenderer {
|
||||
}
|
||||
|
||||
func finalRenderedImage(mirror: Bool = false) -> UIImage? {
|
||||
if let finalTexture = self.resultTexture, let device = self.renderTarget?.mtlDevice {
|
||||
if let finalTexture = self.resultTexture, let device = self.effectiveDevice {
|
||||
return getTextureImage(device: device, texture: finalTexture, mirror: mirror)
|
||||
} else {
|
||||
return nil
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -122,7 +122,10 @@ public extension MediaEditorScreenImpl {
|
||||
return transitionOut
|
||||
}
|
||||
},
|
||||
completion: { result, commit in
|
||||
completion: { results, commit in
|
||||
guard let result = results.first else {
|
||||
return
|
||||
}
|
||||
let entities = generateChatInputTextEntities(result.caption)
|
||||
|
||||
if repost {
|
||||
|
@ -338,7 +338,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
private var isEditingCaption = false
|
||||
private var currentInputMode: MessageInputPanelComponent.InputMode = .text
|
||||
|
||||
private var isSelectionPanelOpen = false
|
||||
fileprivate var isSelectionPanelOpen = false
|
||||
|
||||
private var didInitializeInputMediaNodeDataPromise = false
|
||||
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
|
||||
@ -2013,10 +2013,21 @@ final class MediaEditorScreenComponent: Component {
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
action: { [weak self, weak controller] in
|
||||
if let self, let controller {
|
||||
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
|
||||
if let mediaEditor = controller.node.mediaEditor {
|
||||
if self.isSelectionPanelOpen {
|
||||
mediaEditor.maybePauseVideo()
|
||||
} else {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
mediaEditor.maybeUnpauseVideo()
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state?.updated()
|
||||
|
||||
controller.hapticFeedback.impact(.light)
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
@ -2034,8 +2045,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
|
||||
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
|
||||
transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0)
|
||||
transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0)
|
||||
|
||||
if self.isSelectionPanelOpen {
|
||||
let selectionPanelFrame = CGRect(
|
||||
@ -2061,10 +2072,12 @@ final class MediaEditorScreenComponent: Component {
|
||||
return
|
||||
}
|
||||
self.isSelectionPanelOpen = false
|
||||
self.state?.updated()
|
||||
self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate)
|
||||
|
||||
if let id {
|
||||
controller.node.switchToItem(id)
|
||||
|
||||
controller.hapticFeedback.impact(.light)
|
||||
}
|
||||
},
|
||||
itemSelectionToggled: { [weak self, weak controller] id in
|
||||
@ -2088,6 +2101,8 @@ final class MediaEditorScreenComponent: Component {
|
||||
controller.node.items[fromIndex] = toItem
|
||||
controller.node.items[toIndex] = fromItem
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
|
||||
controller.hapticFeedback.tap()
|
||||
}
|
||||
)
|
||||
),
|
||||
@ -2104,7 +2119,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
selectionPanelView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
}
|
||||
} else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
|
||||
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in
|
||||
selectionPanelView?.removeFromSuperview()
|
||||
})
|
||||
@ -4027,7 +4042,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
if gestureRecognizer === self.dismissPanGestureRecognizer {
|
||||
let location = gestureRecognizer.location(in: self.entitiesView)
|
||||
if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool != nil || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil {
|
||||
if self.controller?.isEmbeddedEditor == true || self.isDisplayingTool != nil || self.entitiesView.hasSelection || self.entitiesView.getView(at: location) != nil || self.componentHostView?.isSelectionPanelOpen == true {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -4188,7 +4203,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
private var previousRotateTimestamp: Double?
|
||||
|
||||
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard !self.isCollageTimelineOpen else {
|
||||
guard !self.isCollageTimelineOpen && !(self.componentHostView?.isSelectionPanelOpen ?? false) else {
|
||||
return
|
||||
}
|
||||
if gestureRecognizer.numberOfTouches == 2, let subject = self.subject, !self.entitiesView.hasSelection {
|
||||
@ -5381,7 +5396,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
var updatedCurrentItem = self.items[currentItemIndex]
|
||||
updatedCurrentItem.caption = self.getCaption()
|
||||
|
||||
if mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values {
|
||||
if (mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values) || updatedCurrentItem.values?.gradientColors == nil {
|
||||
updatedCurrentItem.values = mediaEditor.values
|
||||
updatedCurrentItem.version += 1
|
||||
|
||||
@ -6520,7 +6535,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
public var cancelled: (Bool) -> Void = { _ in }
|
||||
public var willComplete: (UIImage?, Bool, @escaping () -> Void) -> Void
|
||||
public var completion: (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
public var completion: ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
public var dismissed: () -> Void = { }
|
||||
public var willDismiss: () -> Void = { }
|
||||
public var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?
|
||||
@ -6529,7 +6544,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
private var closeFriends = Promise<[EnginePeer]>()
|
||||
private let storiesBlockedPeers: BlockedPeersContext
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
fileprivate let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var audioSessionDisposable: Disposable?
|
||||
private let postingAvailabilityPromise = Promise<StoriesUploadAvailability>()
|
||||
@ -6554,7 +6569,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool, Bool?) -> TransitionOut?,
|
||||
willComplete: @escaping (UIImage?, Bool, @escaping () -> Void) -> Void = { _, _, commit in commit() },
|
||||
completion: @escaping (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
completion: @escaping ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@ -6977,7 +6992,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
let hasPremium = self.context.isPremium
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let title = presentationData.strings.Story_Editor_ExpirationText
|
||||
let currentValue = self.state.privacy.timeout
|
||||
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
|
||||
|
||||
@ -6994,61 +7008,55 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
)
|
||||
}
|
||||
|
||||
var items: [ContextMenuItem] = []
|
||||
items.append(.action(ContextMenuActionItem(text: title, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
|
||||
let timeoutOptions: [(hours: Int, requiresPremium: Bool)] = [
|
||||
(6, true),
|
||||
(12, true),
|
||||
(24, false),
|
||||
(48, true)
|
||||
]
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Story_Editor_ExpirationValue(6), icon: { theme in
|
||||
if !hasPremium {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"), color: theme.contextMenu.secondaryColor)
|
||||
var items: [ContextMenuItem] = [
|
||||
.action(ContextMenuActionItem(
|
||||
text: presentationData.strings.Story_Editor_ExpirationText,
|
||||
textLayout: .multiline,
|
||||
textFont: .small,
|
||||
icon: { _ in return nil },
|
||||
action: emptyAction
|
||||
))
|
||||
]
|
||||
|
||||
for option in timeoutOptions {
|
||||
let text = presentationData.strings.Story_Editor_ExpirationValue(Int32(option.hours))
|
||||
let value = option.hours * 3600
|
||||
|
||||
items.append(.action(ContextMenuActionItem(
|
||||
text: text,
|
||||
icon: { theme in
|
||||
if option.requiresPremium && !hasPremium {
|
||||
return generateTintedImage(
|
||||
image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/TextLockIcon"),
|
||||
color: theme.contextMenu.secondaryColor
|
||||
)
|
||||
} else if currentValue == value {
|
||||
return generateTintedImage(
|
||||
image: UIImage(bundleImageName: "Chat/Context Menu/Check"),
|
||||
color: theme.contextMenu.primaryColor
|
||||
)
|
||||
} else {
|
||||
return currentValue == 3600 * 6 ? generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) : nil
|
||||
return nil
|
||||
}
|
||||
}, action: { [weak self] _, a in
|
||||
},
|
||||
action: { [weak self] _, a in
|
||||
a(.default)
|
||||
|
||||
if hasPremium {
|
||||
updateTimeout(3600 * 6)
|
||||
if !option.requiresPremium || hasPremium {
|
||||
updateTimeout(value)
|
||||
} 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()
|
||||
}
|
||||
})))
|
||||
|
||||
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))
|
||||
@ -7333,23 +7341,328 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return true
|
||||
}
|
||||
|
||||
private var didComplete = false
|
||||
func requestStoryCompletion(animated: Bool) {
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject, !self.didComplete else {
|
||||
private func completeWithMultipleResults(results: [MediaEditorScreenImpl.Result]) {
|
||||
// Send all results to completion handler
|
||||
self.completion(results, { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
finished()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private func processMultipleItems() {
|
||||
guard !self.node.items.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
self.didComplete = true
|
||||
if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = self.node.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
|
||||
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
self.dismissAllTooltips()
|
||||
var updatedCurrentItem = self.node.items[currentItemIndex]
|
||||
updatedCurrentItem.caption = self.node.getCaption()
|
||||
updatedCurrentItem.values = mediaEditor.values
|
||||
self.node.items[currentItemIndex] = updatedCurrentItem
|
||||
}
|
||||
|
||||
mediaEditor.stop()
|
||||
mediaEditor.invalidate()
|
||||
self.node.entitiesView.invalidate()
|
||||
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
|
||||
let totalItems = self.node.items.count
|
||||
|
||||
let context = self.context
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
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) }
|
||||
@ -7407,7 +7720,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
|
||||
self.saveDraft(id: randomId, isEdit: true)
|
||||
|
||||
self.completion(MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in
|
||||
self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
@ -7737,7 +8050,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return
|
||||
}
|
||||
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
|
||||
self.completion(MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId), { [weak self] finished in
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
@ -7754,8 +8067,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
if case let .draft(draft, id) = actualSubject, id == nil {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
||||
}
|
||||
} else {
|
||||
if let image = mediaEditor.resultImage {
|
||||
} else if let image = mediaEditor.resultImage {
|
||||
self.saveDraft(id: randomId)
|
||||
|
||||
var values = mediaEditor.values
|
||||
@ -7764,14 +8076,23 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
outputDimensions = CGSize(width: 640.0, height: 640.0)
|
||||
values = values.withUpdatedQualityPreset(.profile)
|
||||
}
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: outputDimensions, values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in
|
||||
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.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 {
|
||||
@ -7787,6 +8108,30 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func requestStickerCompletion(animated: Bool) {
|
||||
@ -7852,7 +8197,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
#endif
|
||||
|
||||
self.completion(MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size))), { [weak self] finished in
|
||||
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)))], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
@ -7955,7 +8300,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
if isVideo {
|
||||
self.uploadSticker(file, action: .send)
|
||||
} else {
|
||||
self.completion(MediaEditorScreenImpl.Result(
|
||||
self.completion([MediaEditorScreenImpl.Result(
|
||||
media: .sticker(file: file, emoji: self.effectiveStickerEmoji()),
|
||||
mediaAreas: [],
|
||||
caption: NSAttributedString(),
|
||||
@ -7963,7 +8308,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
options: MediaEditorResultPrivacy(sendAsPeerId: nil, privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 0, isForwardingDisabled: false, pin: false),
|
||||
stickers: [],
|
||||
randomId: 0
|
||||
), { [weak self] finished in
|
||||
)], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
Queue.mainQueue().justDispatch {
|
||||
@ -8376,7 +8721,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
result = MediaEditorScreenImpl.Result()
|
||||
}
|
||||
|
||||
self.completion(result, { [weak self] finished in
|
||||
self.completion([result], { [weak self] finished in
|
||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
|
@ -145,23 +145,11 @@ final class SelectionPanelComponent: Component {
|
||||
selectionLayer.lineWidth = lineWidth
|
||||
selectionLayer.frame = selectionFrame
|
||||
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
||||
|
||||
// if !transition.animation.isImmediate {
|
||||
// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
||||
// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
||||
// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
|
||||
// }
|
||||
}
|
||||
|
||||
} else if let selectionLayer = self.selectionLayer {
|
||||
self.selectionLayer = nil
|
||||
selectionLayer.removeFromSuperlayer()
|
||||
|
||||
// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
||||
// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
|
||||
// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
// selectionLayer.removeFromSuperlayer()
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -373,11 +361,96 @@ final class SelectionPanelComponent: Component {
|
||||
}
|
||||
|
||||
func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
|
||||
self.scrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
||||
let buttonFrame = buttonView.convert(buttonView.bounds, to: self)
|
||||
let fromPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y)
|
||||
|
||||
self.scrollView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
|
||||
self.scrollView.layer.animateBounds(from: CGRect(origin: CGPoint(x: buttonFrame.minX - self.scrollView.frame.minX, y: buttonFrame.minY - self.scrollView.frame.minY), size: buttonFrame.size), to: self.scrollView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
self.backgroundMaskPanelView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(16.5)), to: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
|
||||
self.backgroundMaskPanelView.layer.animateBounds(from: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), to: self.backgroundMaskPanelView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
let mainCircleDelay: Double = 0.02
|
||||
let backgroundWidth = self.backgroundMaskPanelView.frame.width
|
||||
for item in component.items {
|
||||
guard let itemView = self.itemViews[item.asset.localIdentifier] else {
|
||||
continue
|
||||
}
|
||||
|
||||
let distance = abs(itemView.frame.center.x - backgroundWidth)
|
||||
let distanceNorm = distance / backgroundWidth
|
||||
let adjustedDistanceNorm = distanceNorm
|
||||
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.14
|
||||
|
||||
itemView.isHidden = true
|
||||
Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in
|
||||
guard let itemView else {
|
||||
return
|
||||
}
|
||||
itemView.isHidden = false
|
||||
itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
|
||||
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 {
|
||||
|
@ -201,7 +201,10 @@ extension PeerInfoScreenImpl {
|
||||
commit()
|
||||
}
|
||||
},
|
||||
completion: { [weak self] result, commit in
|
||||
completion: { [weak self] results, commit in
|
||||
guard let result = results.first else {
|
||||
return
|
||||
}
|
||||
switch result.media {
|
||||
case let .image(image, _):
|
||||
resultImage = image
|
||||
@ -217,7 +220,7 @@ extension PeerInfoScreenImpl {
|
||||
break
|
||||
}
|
||||
dismissImpl?()
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
editorController.cancelled = { _ in
|
||||
cancelled()
|
||||
|
@ -607,10 +607,10 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
||||
return self.profileGifts.buyStarGift(slug: slug, peerId: peerId)
|
||||
},
|
||||
updateResellStars: { [weak self] price in
|
||||
guard let self, case let .unique(uniqueGift) = product.gift else {
|
||||
guard let self, let reference = product.reference else {
|
||||
return
|
||||
}
|
||||
self.profileGifts.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price)
|
||||
self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price)
|
||||
},
|
||||
togglePinnedToTop: { [weak self] pinnedToTop in
|
||||
guard let self else {
|
||||
@ -1479,6 +1479,8 @@ private extension StarGiftReference {
|
||||
return "m_\(messageId.id)"
|
||||
case let .peer(peerId, id):
|
||||
return "p_\(peerId.toInt64())_\(id)"
|
||||
case let .slug(slug):
|
||||
return "s_\(slug)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1315,13 +1315,13 @@ extension ChatControllerImpl {
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
if case let .image(image, _) = result.media {
|
||||
}, completion: { results, commit in
|
||||
if case let .image(image, _) = results.first?.media {
|
||||
completion(image)
|
||||
commit({})
|
||||
}
|
||||
dismissImpl?()
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
editorController.cancelled = { _ in
|
||||
cancelled()
|
||||
@ -1930,17 +1930,17 @@ extension ChatControllerImpl {
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}, completion: { [weak self] result, commit in
|
||||
}, completion: { [weak self] results, commit in
|
||||
dismissImpl?()
|
||||
self?.chatDisplayNode.dismissInput()
|
||||
|
||||
Queue.mainQueue().after(0.1) {
|
||||
commit({})
|
||||
if case let .sticker(file, _) = result.media {
|
||||
if case let .sticker(file, _) = results.first?.media {
|
||||
self?.enqueueStickerFile(file)
|
||||
}
|
||||
}
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
editorController.cancelled = { _ in
|
||||
cancelled()
|
||||
|
@ -3461,9 +3461,9 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
completion(result, commit)
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
}, completion: { results, commit in
|
||||
completion(results.first!, commit)
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
editorController.cancelled = { _ in
|
||||
cancelled()
|
||||
@ -3525,13 +3525,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
if case let .sticker(file, emoji) = result.media {
|
||||
}, completion: { results, commit in
|
||||
if case let .sticker(file, emoji) = results.first?.media {
|
||||
completion(file, emoji, {
|
||||
commit({})
|
||||
})
|
||||
}
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
editorController.cancelled = { _ in
|
||||
cancelled()
|
||||
@ -3558,13 +3558,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
transitionIn: nil,
|
||||
transitionOut: { finished, isNew in
|
||||
return nil
|
||||
}, completion: { result, commit in
|
||||
completion(result, commit)
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
}, completion: { results, commit in
|
||||
completion(results.first!, commit)
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
// editorController.cancelled = { _ in
|
||||
// cancelled()
|
||||
// }
|
||||
return editorController
|
||||
}
|
||||
|
||||
@ -3724,7 +3721,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
transitionOut: { _, _ in
|
||||
return nil
|
||||
},
|
||||
completion: { [weak parentController] result, commit in
|
||||
completion: { [weak parentController] results, commit in
|
||||
guard let result = results.first else {
|
||||
return
|
||||
}
|
||||
let targetPeerId: EnginePeer.Id
|
||||
let target: Stories.PendingTarget
|
||||
if let sendAsPeerId = result.options.sendAsPeerId {
|
||||
|
@ -444,7 +444,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, completion: { [weak self] result, commit in
|
||||
}, completion: { [weak self] results, commit in
|
||||
guard let self else {
|
||||
dismissCameraImpl?()
|
||||
commit({})
|
||||
@ -453,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
|
||||
if let customTarget, case .botPreview = customTarget {
|
||||
externalState.storyTarget = customTarget
|
||||
self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
self.proceedWithStoryUpload(target: customTarget, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
|
||||
dismissCameraImpl?()
|
||||
return
|
||||
@ -464,7 +464,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
target = .peer(id)
|
||||
targetPeerId = id
|
||||
} else {
|
||||
if let sendAsPeerId = result.options.sendAsPeerId {
|
||||
if let sendAsPeerId = results.first?.options.sendAsPeerId {
|
||||
target = .peer(sendAsPeerId)
|
||||
targetPeerId = sendAsPeerId
|
||||
} else {
|
||||
@ -486,12 +486,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
externalState.isPeerArchived = channel.storiesHidden ?? false
|
||||
}
|
||||
|
||||
self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
self.proceedWithStoryUpload(target: target, results: results, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
|
||||
dismissCameraImpl?()
|
||||
})
|
||||
}
|
||||
} as (MediaEditorScreenImpl.Result, @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
} as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void
|
||||
)
|
||||
controller.cancelled = { showDraftTooltip in
|
||||
if showDraftTooltip {
|
||||
|
Loading…
x
Reference in New Issue
Block a user