From b04eee8289363a4ce414250ccb95471a6acea1d1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 27 Nov 2025 19:55:24 +0400 Subject: [PATCH 01/11] Update API [skip ci] --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/AccountContext.swift | 1 + .../AccountContext/Sources/Premium.swift | 1 + submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api15.swift | 68 +++++ submodules/TelegramApi/Sources/Api25.swift | 18 +- submodules/TelegramApi/Sources/Api39.swift | 44 ++- .../ApiUtils/StoreMessage_Telegram.swift | 2 +- .../ApiUtils/TelegramMediaAction.swift | 13 +- .../StandaloneSendMessage.swift | 2 +- .../Sources/State/ManagedRecentStickers.swift | 3 +- .../Sources/State/PendingMessageManager.swift | 4 +- .../Sources/State/Serialization.swift | 2 +- .../SyncCore_TelegramMediaAction.swift | 27 +- .../TelegramEngine/Messages/ForwardGame.swift | 2 +- .../TelegramEngine/Payments/StarGifts.swift | 39 ++- .../Payments/StarGiftsOffers.swift | 55 ++++ .../TelegramEngine/Payments/Stars.swift | 6 +- .../Payments/TelegramEnginePayments.swift | 8 + .../Sources/ServiceMessageStrings.swift | 6 +- .../ChatMessageGiftBubbleContentNode.swift | 4 +- .../Sources/GiftStoreScreen.swift | 2 - .../Sources/GiftViewScreen.swift | 159 +++++++++- .../Sources/ListActionItemComponent.swift | 29 ++ .../Sources/UserApperanceScreen.swift | 3 +- .../Sources/StarsPurchaseScreen.swift | 8 +- .../Sources/StarsWithdrawalScreen.swift | 276 +++++++++++++----- .../Sources/TabSelectorComponent.swift | 20 +- .../ChatInterfaceStateContextMenus.swift | 2 +- .../Sources/SharedAccountContext.swift | 2 + 30 files changed, 687 insertions(+), 125 deletions(-) create mode 100644 submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsOffers.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 44e63f3062..d19b9a7112 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15432,3 +15432,5 @@ Error: %8$@"; "LiveStream.ErrorMaxAllowedEmoji.Text_1" = "You can send up to %d emoji."; "LiveStream.ErrorMaxAllowedEmoji.Text_any" = "You can send up to %d emoji."; + +"Stars.Purchase.StarGiftOfferInfo" = "Buy Stars to make a gift buy offer."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index fa23c3e2f2..5b2029e260 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1178,6 +1178,7 @@ public enum StarsWithdrawalScreenSubject { case enterAmount(current: StarsAmount, minValue: StarsAmount, fractionAfterCommission: Int, kind: PaidMessageKind, completion: (Int64) -> Void) case postSuggestion(channel: EnginePeer, isFromAdmin: Bool, current: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void) case postSuggestionModification(current: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void) + case starGiftOffer(peer: EnginePeer, gift: StarGift.UniqueGift, completion: (CurrencyAmount, Int32) -> Void) } public enum ChannelMembersSearchControllerMode { diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index a97e750a4f..51a2c3f06a 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -144,6 +144,7 @@ public enum StarsPurchasePurpose: Equatable { case sendMessage(peerId: EnginePeer.Id, requiredStars: Int64) case buyStarGift(requiredStars: Int64) case removeOriginalDetailsStarGift(requiredStars: Int64) + case starGiftOffer(requiredStars: Int64) } public struct PremiumConfiguration { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index c21a005241..fc4b470207 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -621,6 +621,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) } dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } dict[-614898352] = { return Api.MessageAction.parse_messageActionStarGift($0) } + dict[2000845012] = { return Api.MessageAction.parse_messageActionStarGiftPurchaseOffer($0) } + dict[1940760427] = { return Api.MessageAction.parse_messageActionStarGiftPurchaseOfferDeclined($0) } dict[-1787656893] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } dict[747579941] = { return Api.MessageAction.parse_messageActionSuggestBirthday($0) } dict[1474192222] = { return Api.MessageAction.parse_messageActionSuggestProfilePhoto($0) } @@ -979,7 +981,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } dict[463097215] = { return Api.StarGift.parse_starGift($0) } - dict[-1329630181] = { return Api.StarGift.parse_starGiftUnique($0) } + dict[-1624264554] = { return Api.StarGift.parse_starGiftUnique($0) } dict[-753154979] = { return Api.StarGiftActiveAuctionState.parse_starGiftActiveAuctionState($0) } dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) } dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index 40303517e8..a526ce3c8c 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -1070,6 +1070,8 @@ public extension Api { case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper) case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?) case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, prepaidUpgradeHash: String?, giftMsgId: Int32?, toId: Api.Peer?) + case messageActionStarGiftPurchaseOffer(flags: Int32, gift: Api.StarGift, price: Api.StarsAmount, expiresAt: Int32) + case messageActionStarGiftPurchaseOfferDeclined(flags: Int32, gift: Api.StarGift, price: Api.StarsAmount) case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleAmount: Api.StarsAmount?, canTransferAt: Int32?, canResellAt: Int32?, dropOriginalDetailsStars: Int64?) case messageActionSuggestBirthday(birthday: Api.Birthday) case messageActionSuggestProfilePhoto(photo: Api.Photo) @@ -1477,6 +1479,23 @@ public extension Api { if Int(flags) & Int(1 << 15) != 0 {serializeInt32(giftMsgId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 18) != 0 {toId!.serialize(buffer, true)} break + case .messageActionStarGiftPurchaseOffer(let flags, let gift, let price, let expiresAt): + if boxed { + buffer.appendInt32(2000845012) + } + serializeInt32(flags, buffer: buffer, boxed: false) + gift.serialize(buffer, true) + price.serialize(buffer, true) + serializeInt32(expiresAt, buffer: buffer, boxed: false) + break + case .messageActionStarGiftPurchaseOfferDeclined(let flags, let gift, let price): + if boxed { + buffer.appendInt32(1940760427) + } + serializeInt32(flags, buffer: buffer, boxed: false) + gift.serialize(buffer, true) + price.serialize(buffer, true) + break case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleAmount, let canTransferAt, let canResellAt, let dropOriginalDetailsStars): if boxed { buffer.appendInt32(-1787656893) @@ -1680,6 +1699,10 @@ public extension Api { return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)]) case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId): return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("giftMsgId", giftMsgId as Any), ("toId", toId as Any)]) + case .messageActionStarGiftPurchaseOffer(let flags, let gift, let price, let expiresAt): + return ("messageActionStarGiftPurchaseOffer", [("flags", flags as Any), ("gift", gift as Any), ("price", price as Any), ("expiresAt", expiresAt as Any)]) + case .messageActionStarGiftPurchaseOfferDeclined(let flags, let gift, let price): + return ("messageActionStarGiftPurchaseOfferDeclined", [("flags", flags as Any), ("gift", gift as Any), ("price", price as Any)]) case .messageActionStarGiftUnique(let flags, let gift, let canExportAt, let transferStars, let fromId, let peer, let savedId, let resaleAmount, let canTransferAt, let canResellAt, let dropOriginalDetailsStars): return ("messageActionStarGiftUnique", [("flags", flags as Any), ("gift", gift as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("resaleAmount", resaleAmount as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any), ("dropOriginalDetailsStars", dropOriginalDetailsStars as Any)]) case .messageActionSuggestBirthday(let birthday): @@ -2473,6 +2496,51 @@ public extension Api { return nil } } + public static func parse_messageActionStarGiftPurchaseOffer(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.StarGift? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarGift + } + var _3: Api.StarsAmount? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount + } + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.MessageAction.messageActionStarGiftPurchaseOffer(flags: _1!, gift: _2!, price: _3!, expiresAt: _4!) + } + else { + return nil + } + } + public static func parse_messageActionStarGiftPurchaseOfferDeclined(_ reader: BufferReader) -> MessageAction? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.StarGift? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarGift + } + var _3: Api.StarsAmount? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageAction.messageActionStarGiftPurchaseOfferDeclined(flags: _1!, gift: _2!, price: _3!) + } + else { + return nil + } + } public static func parse_messageActionStarGiftUnique(_ reader: BufferReader) -> MessageAction? { var _1: Int32? _1 = reader.readInt32() diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 5e4db299ba..4baea64b9d 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -289,7 +289,7 @@ public extension Api { public extension Api { enum StarGift: TypeConstructorDescription { case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?, auctionSlug: String?, giftsPerRound: Int32?) - case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, themePeer: Api.Peer?, peerColor: Api.PeerColor?, hostId: Api.Peer?) + case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, themePeer: Api.Peer?, peerColor: Api.PeerColor?, hostId: Api.Peer?, offerMinStars: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -317,9 +317,9 @@ public extension Api { if Int(flags) & Int(1 << 11) != 0 {serializeString(auctionSlug!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 11) != 0 {serializeInt32(giftsPerRound!, buffer: buffer, boxed: false)} break - case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId): + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId, let offerMinStars): if boxed { - buffer.appendInt32(-1329630181) + buffer.appendInt32(-1624264554) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -349,6 +349,7 @@ public extension Api { if Int(flags) & Int(1 << 10) != 0 {themePeer!.serialize(buffer, true)} if Int(flags) & Int(1 << 11) != 0 {peerColor!.serialize(buffer, true)} if Int(flags) & Int(1 << 12) != 0 {hostId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 13) != 0 {serializeInt32(offerMinStars!, buffer: buffer, boxed: false)} break } } @@ -357,8 +358,8 @@ public extension Api { switch self { case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound): return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any), ("auctionSlug", auctionSlug as Any), ("giftsPerRound", giftsPerRound as Any)]) - case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId): - return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("themePeer", themePeer as Any), ("peerColor", peerColor as Any), ("hostId", hostId as Any)]) + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId, let offerMinStars): + return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("themePeer", themePeer as Any), ("peerColor", peerColor as Any), ("hostId", hostId as Any), ("offerMinStars", offerMinStars as Any)]) } } @@ -486,6 +487,8 @@ public extension Api { if Int(_1!) & Int(1 << 12) != 0 {if let signature = reader.readInt32() { _20 = Api.parse(reader, signature: signature) as? Api.Peer } } + var _21: Int32? + if Int(_1!) & Int(1 << 13) != 0 {_21 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -506,8 +509,9 @@ public extension Api { let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil let _c19 = (Int(_1!) & Int(1 << 11) == 0) || _19 != nil let _c20 = (Int(_1!) & Int(1 << 12) == 0) || _20 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 { - return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, themePeer: _18, peerColor: _19, hostId: _20) + let _c21 = (Int(_1!) & Int(1 << 13) == 0) || _21 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 { + return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, themePeer: _18, peerColor: _19, hostId: _20, offerMinStars: _21) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index f9ccc43e45..a5ed9e4f95 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -5830,9 +5830,9 @@ public extension Api.functions.messages { } } public extension Api.functions.messages { - static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, replyTo: Api.InputReplyTo?, scheduleDate: Int32?, scheduleRepeatPeriod: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?, videoTimestamp: Int32?, allowPaidStars: Int64?, suggestedPost: Api.SuggestedPost?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func forwardMessages(flags: Int32, fromPeer: Api.InputPeer, id: [Int32], randomId: [Int64], toPeer: Api.InputPeer, topMsgId: Int32?, replyTo: Api.InputReplyTo?, scheduleDate: Int32?, scheduleRepeatPeriod: Int32?, sendAs: Api.InputPeer?, quickReplyShortcut: Api.InputQuickReplyShortcut?, effect: Int64?, videoTimestamp: Int32?, allowPaidStars: Int64?, suggestedPost: Api.SuggestedPost?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(1104419550) + buffer.appendInt32(326126204) serializeInt32(flags, buffer: buffer, boxed: false) fromPeer.serialize(buffer, true) buffer.appendInt32(481674261) @@ -5852,10 +5852,11 @@ public extension Api.functions.messages { if Int(flags) & Int(1 << 24) != 0 {serializeInt32(scheduleRepeatPeriod!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 13) != 0 {sendAs!.serialize(buffer, true)} if Int(flags) & Int(1 << 17) != 0 {quickReplyShortcut!.serialize(buffer, true)} + if Int(flags) & Int(1 << 18) != 0 {serializeInt64(effect!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 20) != 0 {serializeInt32(videoTimestamp!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 21) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 23) != 0 {suggestedPost!.serialize(buffer, true)} - return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("replyTo", String(describing: replyTo)), ("scheduleDate", String(describing: scheduleDate)), ("scheduleRepeatPeriod", String(describing: scheduleRepeatPeriod)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut)), ("videoTimestamp", String(describing: videoTimestamp)), ("allowPaidStars", String(describing: allowPaidStars)), ("suggestedPost", String(describing: suggestedPost))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + return (FunctionDescription(name: "messages.forwardMessages", parameters: [("flags", String(describing: flags)), ("fromPeer", String(describing: fromPeer)), ("id", String(describing: id)), ("randomId", String(describing: randomId)), ("toPeer", String(describing: toPeer)), ("topMsgId", String(describing: topMsgId)), ("replyTo", String(describing: replyTo)), ("scheduleDate", String(describing: scheduleDate)), ("scheduleRepeatPeriod", String(describing: scheduleRepeatPeriod)), ("sendAs", String(describing: sendAs)), ("quickReplyShortcut", String(describing: quickReplyShortcut)), ("effect", String(describing: effect)), ("videoTimestamp", String(describing: videoTimestamp)), ("allowPaidStars", String(describing: allowPaidStars)), ("suggestedPost", String(describing: suggestedPost))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in let reader = BufferReader(buffer) var result: Api.Updates? if let signature = reader.readInt32() { @@ -10225,6 +10226,22 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func resolveStarGiftOffer(flags: Int32, offerMsgId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-372344804) + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(offerMsgId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.resolveStarGiftOffer", parameters: [("flags", String(describing: flags)), ("offerMsgId", String(describing: offerMsgId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.payments { static func saveStarGift(flags: Int32, stargift: Api.InputSavedStarGift) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -10262,6 +10279,27 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func sendStarGiftOffer(flags: Int32, peer: Api.InputPeer, slug: String, price: Api.StarsAmount, duration: Int32, randomId: Int64, allowPaidStars: Int64?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1883739327) + serializeInt32(flags, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeString(slug, buffer: buffer, boxed: false) + price.serialize(buffer, true) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt64(randomId, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeInt64(allowPaidStars!, buffer: buffer, boxed: false)} + return (FunctionDescription(name: "payments.sendStarGiftOffer", parameters: [("flags", String(describing: flags)), ("peer", String(describing: peer)), ("slug", String(describing: slug)), ("price", String(describing: price)), ("duration", String(describing: duration)), ("randomId", String(describing: randomId)), ("allowPaidStars", String(describing: allowPaidStars))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.payments { static func sendStarsForm(formId: Int64, invoice: Api.InputInvoice) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index b3790e80e2..f32fbbe7b9 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -232,7 +232,7 @@ func apiMessagePeerIds(_ message: Api.Message) -> [PeerId] { } switch action { - case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions, .messageActionTodoAppendTasks, .messageActionSuggestedPostApproval, .messageActionGiftTon, .messageActionSuggestedPostSuccess, .messageActionSuggestedPostRefund, .messageActionSuggestBirthday: + case .messageActionChannelCreate, .messageActionChatDeletePhoto, .messageActionChatEditPhoto, .messageActionChatEditTitle, .messageActionEmpty, .messageActionPinMessage, .messageActionHistoryClear, .messageActionGameScore, .messageActionPaymentSent, .messageActionPaymentSentMe, .messageActionPhoneCall, .messageActionScreenshotTaken, .messageActionCustomAction, .messageActionBotAllowed, .messageActionSecureValuesSent, .messageActionSecureValuesSentMe, .messageActionContactSignUp, .messageActionGroupCall, .messageActionSetMessagesTTL, .messageActionGroupCallScheduled, .messageActionSetChatTheme, .messageActionChatJoinedByRequest, .messageActionWebViewDataSent, .messageActionWebViewDataSentMe, .messageActionGiftPremium, .messageActionGiftStars, .messageActionTopicCreate, .messageActionTopicEdit, .messageActionSuggestProfilePhoto, .messageActionSetChatWallPaper, .messageActionGiveawayLaunch, .messageActionGiveawayResults, .messageActionBoostApply, .messageActionRequestedPeerSentMe, .messageActionStarGift, .messageActionStarGiftUnique, .messageActionPaidMessagesRefunded, .messageActionPaidMessagesPrice, .messageActionTodoCompletions, .messageActionTodoAppendTasks, .messageActionSuggestedPostApproval, .messageActionGiftTon, .messageActionSuggestedPostSuccess, .messageActionSuggestedPostRefund, .messageActionSuggestBirthday, .messageActionStarGiftPurchaseOffer, .messageActionStarGiftPurchaseOfferDeclined: break case let .messageActionChannelMigrateFrom(_, chatId): result.append(PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 8d2c483acf..9d11d920a3 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -200,7 +200,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0)) + return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0)) case let .messageActionPaidMessagesRefunded(count, stars): return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars)) case let .messageActionPaidMessagesPrice(flags, stars): @@ -274,6 +274,17 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .suggestedPostRefund(TelegramMediaActionType.SuggestedPostRefund(isUserInitiated: (flags & (1 << 0)) != 0))) case let .messageActionSuggestBirthday(birthday): return TelegramMediaAction(action: .suggestedBirthday(TelegramBirthday(apiBirthday: birthday))) + + case let .messageActionStarGiftPurchaseOffer(flags, apiGift, price, expiresAt): + guard let gift = StarGift(apiStarGift: apiGift) else { + return nil + } + return TelegramMediaAction(action: .starGiftPurchaseOffer(gift: gift, amount: CurrencyAmount(apiAmount: price), expireDate: expiresAt, isAccepted: (flags & (1 << 0)) != 0, isDeclined: (flags & (1 << 1)) != 0)) + case let .messageActionStarGiftPurchaseOfferDeclined(flags, apiGift, price): + guard let gift = StarGift(apiStarGift: apiGift) else { + return nil + } + return TelegramMediaAction(action: .starGiftPurchaseOfferDeclined(gift: gift, amount: CurrencyAmount(apiAmount: price), hasExpired: (flags & (1 << 0)) != 0)) } } diff --git a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift index 387b022a43..c4a52df96a 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/StandaloneSendMessage.swift @@ -499,7 +499,7 @@ private func sendUploadedMessageContent( } if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: nil, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: nil), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: nil, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: nil), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index bbca2809d5..0413196f78 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -363,7 +363,8 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne flags: [], themePeerId: nil, peerColor: nil, - hostPeerId: nil + hostPeerId: nil, + minOfferStars: nil ) if let entry = CodableEntry(RecentStarGiftItem(gift)) { items.append(OrderedItemListEntry(id: RecentStarGiftItemId(id).rawValue, contents: entry)) diff --git a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift index 42c4920826..cbdf0a3a1b 100644 --- a/submodules/TelegramCore/Sources/State/PendingMessageManager.swift +++ b/submodules/TelegramCore/Sources/State/PendingMessageManager.swift @@ -1133,7 +1133,7 @@ public final class PendingMessageManager { } else if let inputSourcePeerId = forwardPeerIds.first, let inputSourcePeer = transaction.getPeer(inputSourcePeerId).flatMap(apiInputPeer) { let dependencyTag = PendingMessageRequestDependencyTag(messageId: messages[0].0.id) - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: inputSourcePeer, id: forwardIds.map { $0.0.id }, randomId: forwardIds.map { $0.1 }, toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, effect: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag) } else { assertionFailure() sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "Invalid forward source")) @@ -1831,7 +1831,7 @@ public final class PendingMessageManager { } if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) { - sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag) + sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: replyTo, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: quickReplyShortcut, effect: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag) |> map(NetworkRequestResult.result) } else { sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal")) diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index 69094986ec..0a8680497f 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 219 + return 220 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 549e5dd77a..82dcbc27a4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -256,7 +256,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?) - case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool) + case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) case conferenceCall(ConferenceCall) @@ -267,6 +267,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case suggestedPostSuccess(amount: CurrencyAmount) case suggestedPostRefund(SuggestedPostRefund) case suggestedBirthday(TelegramBirthday) + case starGiftPurchaseOffer(gift: StarGift, amount: CurrencyAmount, expireDate: Int32, isAccepted: Bool, isDeclined: Bool) + case starGiftPurchaseOfferDeclined(gift: StarGift, amount: CurrencyAmount, hasExpired: Bool) public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -402,7 +404,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else if let stars = decoder.decodeOptionalInt64ForKey("resaleStars") { resaleAmount = CurrencyAmount(amount: StarsAmount(value: stars, nanos: 0), currency: .stars) } - self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false)) + self = .starGiftUnique(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, isUpgrade: decoder.decodeBoolForKey("isUpgrade", orElse: false), isTransferred: decoder.decodeBoolForKey("isTransferred", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), canExportDate: decoder.decodeOptionalInt32ForKey("canExportDate"), transferStars: decoder.decodeOptionalInt64ForKey("transferStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), resaleAmount: resaleAmount, canTransferDate: decoder.decodeOptionalInt32ForKey("canTransferDate"), canResaleDate: decoder.decodeOptionalInt32ForKey("canResaleDate"), dropOriginalDetailsStars: decoder.decodeOptionalInt64ForKey("dropOriginalDetailsStars"), assigned: decoder.decodeBoolForKey("assigned", orElse: false), fromOffer: decoder.decodeBoolForKey("fromOffer", orElse: false)) case 46: self = .paidMessagesRefunded(count: decoder.decodeInt32ForKey("count", orElse: 0), stars: decoder.decodeInt64ForKey("stars", orElse: 0)) case 47: @@ -434,6 +436,10 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .suggestedPostRefund(decoder.decodeCodable(SuggestedPostRefund.self, forKey: "s") ?? SuggestedPostRefund(isUserInitiated: true)) case 55: self = .suggestedBirthday(decoder.decodeCodable(TelegramBirthday.self, forKey: "birthday") ?? TelegramBirthday(day: 1, month: 1, year: nil)) + case 56: + self = .starGiftPurchaseOffer(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, amount: decoder.decodeCodable(CurrencyAmount.self, forKey: "amount")!, expireDate: decoder.decodeInt32ForKey("expireDate", orElse: 0), isAccepted: decoder.decodeBoolForKey("isAccepted", orElse: false), isDeclined: decoder.decodeBoolForKey("isDeclined", orElse: false)) + case 57: + self = .starGiftPurchaseOfferDeclined(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, amount: decoder.decodeCodable(CurrencyAmount.self, forKey: "amount")!, hasExpired: decoder.decodeBoolForKey("hasExpired", orElse: false)) default: self = .unknown } @@ -792,7 +798,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "toPeerId") } - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") encoder.encodeBool(isUpgrade, forKey: "isUpgrade") @@ -846,6 +852,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeNil(forKey: "dropOriginalDetailsStars") } encoder.encodeBool(assigned, forKey: "assigned") + encoder.encodeBool(fromOffer, forKey: "fromOffer") case let .paidMessagesRefunded(count, stars): encoder.encodeInt32(46, forKey: "_rawValue") encoder.encodeInt32(count, forKey: "count") @@ -899,6 +906,18 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .suggestedBirthday(birthday): encoder.encodeInt32(55, forKey: "_rawValue") encoder.encodeCodable(birthday, forKey: "birthday") + case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined): + encoder.encodeInt32(56, forKey: "_rawValue") + encoder.encodeObject(gift, forKey: "gift") + encoder.encodeCodable(amount, forKey: "amount") + encoder.encodeInt32(expireDate, forKey: "expireDate") + encoder.encodeBool(isAccepted, forKey: "isAccepted") + encoder.encodeBool(isDeclined, forKey: "isDeclined") + case let .starGiftPurchaseOfferDeclined(gift, amount, hasExpired): + encoder.encodeInt32(57, forKey: "_rawValue") + encoder.encodeObject(gift, forKey: "gift") + encoder.encodeCodable(amount, forKey: "amount") + encoder.encodeBool(hasExpired, forKey: "hasExpired") } } @@ -941,7 +960,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { peerIds.append(toPeerId) } return peerIds - case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _): + case let .starGiftUnique(gift, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, _, _): var peerIds: [PeerId] = [] if let peerId { peerIds.append(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift index 584f8775a8..95e6913650 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ForwardGame.swift @@ -14,7 +14,7 @@ func _internal_forwardGameWithScore(account: Account, messageId: MessageId, to p flags |= (1 << 13) } - return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, videoTimestamp: nil, allowPaidStars: nil, suggestedPost: nil)) + return account.network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: fromInputPeer, id: [messageId.id], randomId: [Int64.random(in: Int64.min ... Int64.max)], toPeer: toInputPeer, topMsgId: threadId.flatMap { Int32(clamping: $0) }, replyTo: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, videoTimestamp: nil, allowPaidStars: nil, suggestedPost: nil)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 66b92fb194..dc7ce90d2a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -346,6 +346,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case themePeerId case peerColor case hostPeerId + case minOfferStars } public struct Flags: OptionSet { @@ -635,8 +636,9 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public let themePeerId: EnginePeer.Id? public let peerColor: PeerCollectibleColor? public let hostPeerId: EnginePeer.Id? + public let minOfferStars: Int64? - public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, flags: Flags, themePeerId: EnginePeer.Id?, peerColor: PeerCollectibleColor?, hostPeerId: EnginePeer.Id?) { + public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, flags: Flags, themePeerId: EnginePeer.Id?, peerColor: PeerCollectibleColor?, hostPeerId: EnginePeer.Id?, minOfferStars: Int64?) { self.id = id self.giftId = giftId self.title = title @@ -655,6 +657,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.themePeerId = themePeerId self.peerColor = peerColor self.hostPeerId = hostPeerId + self.minOfferStars = minOfferStars } public init(from decoder: Decoder) throws { @@ -691,6 +694,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.themePeerId = try container.decodeIfPresent(Int64.self, forKey: .themePeerId).flatMap { EnginePeer.Id($0) } self.peerColor = try container.decodeIfPresent(PeerCollectibleColor.self, forKey: .peerColor) self.hostPeerId = try container.decodeIfPresent(Int64.self, forKey: .hostPeerId).flatMap { EnginePeer.Id($0) } + self.minOfferStars = try container.decodeIfPresent(Int64.self, forKey: .minOfferStars) } public init(decoder: PostboxDecoder) { @@ -726,6 +730,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.themePeerId = decoder.decodeOptionalInt64ForKey(CodingKeys.themePeerId.rawValue).flatMap { EnginePeer.Id($0) } self.peerColor = decoder.decodeCodable(PeerCollectibleColor.self, forKey: CodingKeys.peerColor.rawValue) self.hostPeerId = decoder.decodeOptionalInt64ForKey(CodingKeys.hostPeerId.rawValue).flatMap { EnginePeer.Id($0) } + self.minOfferStars = decoder.decodeOptionalInt64ForKey(CodingKeys.minOfferStars.rawValue) } public func encode(to encoder: Encoder) throws { @@ -755,6 +760,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encodeIfPresent(self.themePeerId?.toInt64(), forKey: .themePeerId) try container.encodeIfPresent(self.peerColor, forKey: .peerColor) try container.encodeIfPresent(self.hostPeerId?.toInt64(), forKey: .hostPeerId) + try container.encodeIfPresent(self.minOfferStars, forKey: .minOfferStars) } public func encode(_ encoder: PostboxEncoder) { @@ -812,6 +818,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding { } else { encoder.encodeNil(forKey: CodingKeys.hostPeerId.rawValue) } + if let minOfferStars = self.minOfferStars { + encoder.encodeInt64(minOfferStars, forKey: CodingKeys.minOfferStars.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.minOfferStars.rawValue) + } } public func withResellAmounts(_ resellAmounts: [CurrencyAmount]?) -> UniqueGift { @@ -833,7 +844,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { flags: self.flags, themePeerId: self.themePeerId, peerColor: self.peerColor, - hostPeerId: self.hostPeerId + hostPeerId: self.hostPeerId, + minOfferStars: self.minOfferStars ) } @@ -856,7 +868,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { flags: self.flags, themePeerId: self.themePeerId, peerColor: self.peerColor, - hostPeerId: self.hostPeerId + hostPeerId: self.hostPeerId, + minOfferStars: self.minOfferStars ) } @@ -879,7 +892,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { flags: self.flags, themePeerId: themePeerId, peerColor: self.peerColor, - hostPeerId: self.hostPeerId + hostPeerId: self.hostPeerId, + minOfferStars: self.minOfferStars ) } @@ -902,7 +916,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { flags: self.flags, themePeerId: self.themePeerId, peerColor: self.peerColor, - hostPeerId: self.hostPeerId + hostPeerId: self.hostPeerId, + minOfferStars: self.minOfferStars ) } } @@ -1027,7 +1042,7 @@ extension StarGift { return nil } self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit, lockedUntilDate: lockedUntilDate, auctionSlug: auctionSlug, auctionGiftsPerRound: giftsPerRound)) - case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, themePeer, peerColor, hostPeerId): + case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, themePeer, peerColor, hostPeerId, minOfferStars): let owner: StarGift.UniqueGift.Owner if let ownerAddress { owner = .address(ownerAddress) @@ -1059,7 +1074,7 @@ extension StarGift { break } - self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (apiFlags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency, flags: flags, themePeerId: themePeer?.peerId, peerColor: peerCollectibleColor, hostPeerId: hostPeerId?.peerId)) + self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (apiFlags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency, flags: flags, themePeerId: themePeer?.peerId, peerColor: peerCollectibleColor, hostPeerId: hostPeerId?.peerId, minOfferStars: minOfferStars.flatMap { Int64($0) })) } } } @@ -1270,7 +1285,7 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags) } var media = currentMessage.media - if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned) = action.action, case let .unique(uniqueGift) = gift { + if let action = media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer) = action.action, case let .unique(uniqueGift) = gift { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } media = [ TelegramMediaAction( @@ -1290,7 +1305,9 @@ func _internal_dropStarGiftOriginalDetails(account: Account, reference: StarGift canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: nil, - assigned: assigned) + assigned: assigned, + fromOffer: fromOffer + ) ) ] } @@ -1395,7 +1412,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star case let .updateNewMessage(message, _, _): if let message = StoreMessage(apiMessage: message, accountPeerId: account.peerId, peerIsForum: false) { for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _) = action.action, case let .Id(messageId) = message.id { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) @@ -1422,7 +1439,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star collectionIds: nil, prepaidUpgradeHash: nil, upgradeSeparate: false, - dropOriginalDetailsStars: dropOriginalDetailsStars + dropOriginalDetailsStars: dropOriginalDetailsStars )) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsOffers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsOffers.swift new file mode 100644 index 0000000000..acb147757a --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsOffers.swift @@ -0,0 +1,55 @@ +import Foundation +import Postbox +import MtProtoKit +import SwiftSignalKit +import TelegramApi + +public enum ResolveStarGiftOfferError { + case generic +} + +func _internal_resolveStarGiftOffer(account: Account, messageId: EngineMessage.Id, accept: Bool) -> Signal { + var flags: Int32 = 0 + if !accept { + flags |= (1 << 0) + } + return account.network.request(Api.functions.payments.resolveStarGiftOffer(flags: flags, offerMsgId: messageId.id)) + |> mapError { _ -> ResolveStarGiftOfferError in + return .generic + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + return .complete() + } + |> ignoreValues +} + + +public enum SendStarGiftOfferError { + case generic +} + +func _internal_sendStarGiftOffer(account: Account, peerId: EnginePeer.Id, slug: String, amount: CurrencyAmount, duration: Int32, allowPaidStars: Int64?) -> Signal { + var flags: Int32 = 0 + if let _ = allowPaidStars { + flags |= (1 << 0) + } + return account.postbox.transaction { transaction in + return transaction.getPeer(peerId).flatMap(apiInputPeer) + } + |> castError(SendStarGiftOfferError.self) + |> mapToSignal { inputPeer -> Signal in + guard let inputPeer else { + return .fail(.generic) + } + return account.network.request(Api.functions.payments.sendStarGiftOffer(flags: flags, peer: inputPeer, slug: slug, price: amount.apiAmount, duration: duration, randomId: Int64.random(in: .min ..< .max), allowPaidStars: allowPaidStars)) + |> mapError { _ -> SendStarGiftOfferError in + return .generic + } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) + return .complete() + } + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 4bddd073cc..902808ae31 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -735,6 +735,9 @@ private extension StarsContext.State.Transaction { if (apiFlags & (1 << 28)) != 0 { flags.insert(.isStarGiftAuctionBid) } + if (apiFlags & (1 << 29)) != 0 { + flags.insert(.isStarGiftOffer) + } let media = extendedMedia.flatMap({ $0.compactMap { textMediaAndExpirationTimerFromApiMedia($0, PeerId(0)).media } }) ?? [] @@ -793,6 +796,7 @@ public final class StarsContext { public static let isStarGiftDropOriginalDetails = Flags(rawValue: 1 << 12) public static let isStarGiftAuctionBid = Flags(rawValue: 1 << 13) public static let isLiveStreamPaidMessage = Flags(rawValue: 1 << 14) + public static let isStarGiftOffer = Flags(rawValue: 1 << 15) } public enum Peer: Equatable { @@ -1640,7 +1644,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot case .giftCode, .stars, .starsGift, .starsChatSubscription, .starGift, .starGiftUpgrade, .starGiftTransfer, .premiumGift, .starGiftResale, .starGiftPrepaidUpgrade, .starGiftDropOriginalDetails, .starGiftAuctionBid: receiptMessageId = nil } - } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _) = action.action, case let .Id(messageId) = message.id { + } else if case let .starGiftUnique(gift, _, _, savedToProfile, canExportDate, transferStars, _, _, peerId, _, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _) = action.action, case let .Id(messageId) = message.id { let reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index f92f22385b..df269f3a72 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -180,6 +180,14 @@ public extension TelegramEngine { public func getStarsTransaction(reference: StarsTransactionReference) -> Signal { return _internal_getStarsTransaction(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, transactionReference: reference) } + + public func resolveStarGiftOffer(messageId: EngineMessage.Id, accept: Bool) -> Signal { + return _internal_resolveStarGiftOffer(account: self.account, messageId: messageId, accept: accept) + } + + public func sendStarGiftOffer(peerId: EnginePeer.Id, slug: String, amount: CurrencyAmount, duration: Int32, allowPaidStars: Int64?) -> Signal { + return _internal_sendStarGiftOffer(account: self.account, peerId: peerId, slug: slug, amount: amount, duration: duration, allowPaidStars: allowPaidStars) + } } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 3855648936..8c972d6962 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1262,7 +1262,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, _): if case let .unique(gift) = gift { if !forAdditionalServiceMessage && !"".isEmpty { attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor) @@ -1616,6 +1616,10 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } + case .starGiftPurchaseOffer: + attributedString = nil + case .starGiftPurchaseOfferDeclined: + attributedString = nil case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 4aef351ad9..b65bd9c04a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -282,7 +282,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { switch action.action { case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy - case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy default: break @@ -686,7 +686,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, _): if case let .unique(uniqueGift) = gift { isStarGift = true diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index fa59cfe9e3..f8fcc34ce1 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -167,8 +167,6 @@ final class GiftStoreScreenComponent: Component { return } - //, self.state?.starGiftsState?.dataState != .loading - let availableWidth = self.scrollView.bounds.width let availableHeight = self.scrollView.bounds.height let contentOffset = self.scrollView.contentOffset.y diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 57e43cc4c2..ccb673df45 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -720,8 +720,7 @@ private final class GiftViewSheetContent: CombinedComponent { if let dropOriginalDetailsImpl { signal = dropOriginalDetailsImpl(reference) } else { - signal = (context.engine.payments.dropStarGiftOriginalDetails(reference: reference) - |> deliverOnMainQueue) + signal = context.engine.payments.dropStarGiftOriginalDetails(reference: reference) } self.upgradeDisposable = (signal @@ -738,7 +737,7 @@ private final class GiftViewSheetContent: CombinedComponent { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } self.subject = .profileGift(peerId, gift.withGift(.unique(uniqueGift.withAttributes(updatedAttributes)))) case let .message(message): - if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned) = action.action, case let .unique(uniqueGift) = gift { + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, _, assigned, fromOffer) = action.action, case let .unique(uniqueGift) = gift { let updatedAttributes = uniqueGift.attributes.filter { $0.attributeType != .originalInfo } let updatedMedia: [Media] = [ TelegramMediaAction( @@ -758,7 +757,9 @@ private final class GiftViewSheetContent: CombinedComponent { canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: nil, - assigned: assigned) + assigned: assigned, + fromOffer: fromOffer + ) ) ] @@ -1323,7 +1324,7 @@ private final class GiftViewSheetContent: CombinedComponent { } } - func viewUpgradedGift(messageId: EngineMessage.Id) { + func viewUpgradedGift(messageId: EngineMessage.Id, delay: Bool) { guard let controller = self.getController(), let navigationController = controller.navigationController as? NavigationController else { return } @@ -1334,7 +1335,17 @@ private final class GiftViewSheetContent: CombinedComponent { guard let self, let navigationController, let peer else { return } - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true, purposefulAction: {}, peekData: nil, forceAnimatedScroll: true)) + let action = { + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false), keepStack: .always, useExisting: true, purposefulAction: {}, peekData: nil, forceAnimatedScroll: true)) + } + + if delay { + Queue.mainQueue().after(0.3) { + action() + } + } else { + action() + } }) } @@ -1425,6 +1436,17 @@ private final class GiftViewSheetContent: CombinedComponent { self?.shareGift() }))) + + //TODO:localize + if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId { + items.append(.action(ContextMenuActionItem(text: "Offer to Buy", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, _ in + c?.dismiss(completion: nil) + + self?.openGiftBuyOffer() + }))) + } if gift.flags.contains(.isThemeAvailable) { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Gift_View_Context_SetAsTheme, icon: { theme in @@ -2310,6 +2332,118 @@ private final class GiftViewSheetContent: CombinedComponent { controller.switchToNextUpgradable() } } + + func openGiftBuyOffer() { + guard let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, case let .peerId(ownerPeerId) = uniqueGift.owner, let controller = self.getController() else { + return + } + let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: ownerPeerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer else { + return + } + let buyController = self.context.sharedContext.makeStarsWithdrawalScreen(context: self.context, subject: .starGiftOffer(peer: peer, gift: uniqueGift, completion: { [weak self] amount, duration in + guard let self else { + return + } + self.commitGiftBuyOffer(peer: peer, price: amount, duration: duration) + })) + controller.push(buyController) + }) + } + + func commitGiftBuyOffer(peer: EnginePeer, price: CurrencyAmount, duration: Int32) { + guard let gift = self.subject.arguments?.gift, case let .unique(uniqueGift) = gift, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { + return + } + + let context = self.context + let proceed = { [weak self, weak starsContext] in + guard let self else { + return + } + self.upgradeDisposable = (context.engine.payments.sendStarGiftOffer(peerId: peer.id, slug: uniqueGift.slug, amount: price, duration: duration, allowPaidStars: nil) + |> deliverOnMainQueue).start(error: { _ in + }, completed: { [weak self, weak starsContext] in + guard let self else { + return + } + Queue.mainQueue().after(0.5) { + starsContext?.load(force: true) + } + self.openPeer(peer, dismiss: true) + }) + } + + if price.currency == .stars, starsState.balance < price.amount { + let _ = (self.starsTopUpOptionsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak self] options in + guard let self, let controller = self.getController() else { + return + } + let purchaseController = context.sharedContext.makeStarsPurchaseScreen( + context: context, + starsContext: starsContext, + options: options ?? [], + purpose: .starGiftOffer(requiredStars: price.amount.value), + targetPeerId: nil, + customTheme: nil, + completion: { [weak self, weak starsContext] stars in + guard let self, let starsContext else { + return + } + self.inProgress = true + self.updated() + + starsContext.add(balance: StarsAmount(value: stars, nanos: 0)) + let _ = (starsContext.onUpdate + |> deliverOnMainQueue).start(next: { [weak self] in + guard let self else { + return + } + Queue.mainQueue().after(0.1, { [weak self] in + guard let self, let starsContext = self.context.starsContext, let starsState = starsContext.currentState else { + return + } + if starsState.balance < price.amount { + self.inProgress = false + self.updated() + + self.commitGiftBuyOffer(peer: peer, price: price, duration: duration) + } else { + proceed() + } + }); + }) + } + ) + controller.push(purchaseController) + }) + } else if price.currency == .ton, let tonState = context.tonContext?.currentState, tonState.balance < price.amount { + guard let controller = self.getController() else { + return + } + let needed = price.amount - tonState.balance + var fragmentUrl = "https://fragment.com/ads/topup" + if let data = self.context.currentAppConfiguration.with({ $0 }).data, let value = data["ton_topup_url"] as? String { + fragmentUrl = value + } + controller.push(BalanceNeededScreen( + context: self.context, + amount: needed, + buttonAction: { [weak self] in + guard let self else { + return + } + self.context.sharedContext.applicationBindings.openUrl(fragmentUrl) + } + )) + } else { + proceed() + } + } } func makeState() -> State { @@ -4735,7 +4869,14 @@ private final class GiftViewSheetContent: CombinedComponent { transition: .spring(duration: 0.2) ) } else if upgraded, let arguments = subject.arguments, let upgradeMessageIdId = arguments.upgradeMessageId, let originalMessageId = arguments.messageId, !arguments.upgradeSeparate { - let upgradeMessageId = MessageId(peerId: originalMessageId.peerId, namespace: originalMessageId.namespace, id: upgradeMessageIdId) + var delay = false + var peerId: EnginePeer.Id = originalMessageId.peerId + if peerId.isTelegramNotifications { + peerId = component.context.account.peerId + delay = true + } + + let upgradeMessageId = MessageId(peerId: peerId, namespace: originalMessageId.namespace, id: upgradeMessageIdId) let buttonTitle = strings.Gift_View_ViewUpgraded buttonChild = button.update( component: ButtonComponent( @@ -4748,7 +4889,7 @@ private final class GiftViewSheetContent: CombinedComponent { displaysProgress: false, action: { [weak state] in state?.dismiss(animated: true) - state?.viewUpgradedGift(messageId: upgradeMessageId) + state?.viewUpgradedGift(messageId: upgradeMessageId, delay: delay) }), availableSize: buttonSize, transition: context.transition @@ -5216,7 +5357,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let fromPeerId = senderId ?? message.author?.id return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId) - case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _): + case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _): var reference: StarGiftReference if let peerId, let savedId { reference = .peer(peerId: peerId, id: savedId) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index d9abc93708..05b33fcc04 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -70,6 +70,7 @@ public final class ListActionItemComponent: Component { case arrow case toggle(Toggle) case activity + case expandArrows case custom(CustomAccessory) } @@ -373,6 +374,8 @@ public final class ListActionItemComponent: Component { } case .arrow: contentRightInset = 30.0 + case .expandArrows: + contentRightInset = 36.0 case .toggle: contentRightInset = 76.0 case .activity: @@ -645,6 +648,32 @@ public final class ListActionItemComponent: Component { } } + if case .expandArrows = component.accessory { + let arrowView: UIImageView + var arrowTransition = transition + if let current = self.arrowView { + arrowView = current + if themeUpdated { + arrowView.image = PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme) + } + } else { + arrowTransition = arrowTransition.withAnimation(.none) + arrowView = UIImageView(image: PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme)) + self.arrowView = arrowView + self.addSubview(arrowView) + } + + if let image = arrowView.image { + let arrowFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - image.size.width, y: floor((contentHeight - image.size.height) * 0.5)), size: image.size) + arrowTransition.setFrame(view: arrowView, frame: arrowFrame) + } + } else { + if let arrowView = self.arrowView { + self.arrowView = nil + arrowView.removeFromSuperview() + } + } + if case let .toggle(toggle) = component.accessory { switch toggle.style { case .regular: diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index 20d3361b50..be6a5431ad 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -777,7 +777,8 @@ final class UserAppearanceScreenComponent: Component { flags: [], themePeerId: nil, peerColor: nil, - hostPeerId: nil + hostPeerId: nil, + minOfferStars: nil ) signal = component.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: emojiStatus.expirationDate) } else { diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index d30e7eac64..b0a69117a4 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -253,6 +253,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { textString = strings.Stars_Purchase_BuyStarGiftInfo case .removeOriginalDetailsStarGift: textString = strings.Stars_Purchase_RemoveOriginalDetailsStarGiftInfo + case .starGiftOffer: + textString = strings.Stars_Purchase_StarGiftOfferInfo } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in @@ -843,7 +845,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { titleText = strings.Stars_Purchase_GetStars case .gift: titleText = strings.Stars_Purchase_GiftStars - case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars), let .buyStarGift(requiredStars), let .removeOriginalDetailsStarGift(requiredStars): + case let .topUp(requiredStars, _), let .transfer(_, requiredStars), let .reactions(_, requiredStars), let .subscription(_, requiredStars, _), let .unlockMedia(requiredStars), let .starGift(_, requiredStars), let .upgradeStarGift(requiredStars), let .transferStarGift(requiredStars), let .sendMessage(_, requiredStars), let .buyStarGift(requiredStars), let .removeOriginalDetailsStarGift(requiredStars), let .starGiftOffer(requiredStars): titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) } @@ -1312,6 +1314,10 @@ private extension StarsPurchasePurpose { return requiredStars case let .buyStarGift(requiredStars): return requiredStars + case let .removeOriginalDetailsStarGift(requiredStars): + return requiredStars + case let .starGiftOffer(requiredStars): + return requiredStars default: return nil } diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index 3a3a2ee0b3..a1ebcb086f 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -61,6 +61,7 @@ private final class SheetContent: CombinedComponent { let currencyToggle = Child(TabSelectorComponent.self) let amountSection = Child(ListSectionComponent.self) let amountAdditionalLabel = Child(MultilineTextComponent.self) + let periodSection = Child(ListSectionComponent.self) let timestampSection = Child(ListSectionComponent.self) let onlyTonSection = Child(ListSectionComponent.self) let button = Child(ButtonComponent.self) @@ -177,7 +178,7 @@ private final class SheetContent: CombinedComponent { if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero { let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" } case .reaction: titleString = environment.strings.Stars_SendStars_Title @@ -231,10 +232,33 @@ private final class SheetContent: CombinedComponent { switch state.currency { case .stars: let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" case .ton: let usdRate = Double(tonUsdRate) / 1000.0 / 1000000.0 - amountLabel = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + } + } + case .starGiftOffer: + titleString = "Offer to Buy" + switch state.currency { + case .stars: + amountTitle = "STARS TO OFFER" + minAmount = StarsAmount(value: resaleConfiguration.channelMessageSuggestionMinStarsAmount, nanos: 0) + case .ton: + amountTitle = "TON TO OFFER" + minAmount = StarsAmount(value: 0, nanos: 0) + } + maxAmount = nil + amountPlaceholder = "Price" + + if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate, let tonUsdRate = withdrawConfiguration.tonUsdRate, let amount = state.amount, amount > StarsAmount.zero { + switch state.currency { + case .stars: + let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 + amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + case .ton: + let usdRate = Double(tonUsdRate) / 1000.0 / 1000000.0 + amountLabel = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" } } } @@ -309,8 +333,12 @@ private final class SheetContent: CombinedComponent { tonBalanceValue = tonBalance } - if case let .suggestedPost(mode, _, _, _) = component.mode { - var displayCurrencySelector = false + let selectedId: AnyHashable = state.currency == .stars ? AnyHashable(0 as Int) : AnyHashable(1 as Int) + let starsTitle: String + let tonTitle: String + var displayCurrencySelector = false + switch component.mode { + case let .suggestedPost(mode, _, _, _): switch mode { case let .sender(_, isFromAdmin): if isFromAdmin { @@ -320,74 +348,74 @@ private final class SheetContent: CombinedComponent { displayCurrencySelector = true } } + starsTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferStars + tonTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferTon case .admin: displayCurrencySelector = true + starsTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestStars + tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon } - - if displayCurrencySelector { - let selectedId: AnyHashable = state.currency == .stars ? AnyHashable(0 as Int) : AnyHashable(1 as Int) - let starsTitle: String - let tonTitle: String - switch mode { - case .sender: - starsTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferStars - tonTitle = environment.strings.Chat_PostSuggestion_Suggest_OfferTon - case .admin: - starsTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestStars - tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon - } - - let currencyToggle = currencyToggle.update( - component: TabSelectorComponent( - colors: TabSelectorComponent.Colors( - foreground: theme.list.itemSecondaryTextColor, - selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), - simple: true - ), - theme: theme, - customLayout: TabSelectorComponent.CustomLayout( - font: Font.medium(14.0), - spacing: 10.0 - ), - items: [ - TabSelectorComponent.Item( - id: AnyHashable(0), - content: .component(AnyComponent(CurrencyTabItemComponent(icon: .stars, title: starsTitle, theme: theme))) - ), - TabSelectorComponent.Item( - id: AnyHashable(1), - content: .component(AnyComponent(CurrencyTabItemComponent(icon: .ton, title: tonTitle, theme: theme))) - ) - ], - selectedId: selectedId, - setSelectedId: { [weak state] id in - guard let state else { - return - } - - let currency: CurrencyAmount.Currency - if id == AnyHashable(0) { - currency = .stars - } else { - currency = .ton - } - if state.currency != currency { - state.currency = currency - state.amount = nil - } - state.updated(transition: .spring(duration: 0.4)) - } + case .starGiftOffer: + displayCurrencySelector = true + starsTitle = "Offer Stars" + tonTitle = "Offer TON" + default: + starsTitle = "" + tonTitle = "" + } + + if displayCurrencySelector { + let currencyToggle = currencyToggle.update( + component: TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: theme.list.itemSecondaryTextColor, + selection: theme.list.itemSecondaryTextColor.withMultipliedAlpha(0.15), + simple: true ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), - transition: context.transition - ) - contentSize.height -= 17.0 - let currencyToggleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - currencyToggle.size.width) * 0.5), y: contentSize.height), size: currencyToggle.size) - context.add(currencyToggle - .position(currencyToggle.size.centered(in: currencyToggleFrame).center)) - - contentSize.height += currencyToggle.size.height + 29.0 - } + theme: theme, + style: .glass, + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 10.0 + ), + items: [ + TabSelectorComponent.Item( + id: AnyHashable(0), + content: .component(AnyComponent(CurrencyTabItemComponent(icon: .stars, title: starsTitle, theme: theme))) + ), + TabSelectorComponent.Item( + id: AnyHashable(1), + content: .component(AnyComponent(CurrencyTabItemComponent(icon: .ton, title: tonTitle, theme: theme))) + ) + ], + selectedId: selectedId, + setSelectedId: { [weak state] id in + guard let state else { + return + } + + let currency: CurrencyAmount.Currency + if id == AnyHashable(0) { + currency = .stars + } else { + currency = .ton + } + if state.currency != currency { + state.currency = currency + state.amount = nil + } + state.updated(transition: .spring(duration: 0.4)) + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 100.0), + transition: context.transition + ) + contentSize.height -= 17.0 + let currencyToggleFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - currencyToggle.size.width) * 0.5), y: contentSize.height), size: currencyToggle.size) + context.add(currencyToggle + .position(currencyToggle.size.centered(in: currencyToggleFrame).center)) + + contentSize.height += currencyToggle.size.height + 29.0 } let amountFont = Font.regular(13.0) @@ -446,7 +474,7 @@ private final class SheetContent: CombinedComponent { if let usdWithdrawRate = withdrawConfiguration.usdWithdrawRate { let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0 - amountRightLabel = "≈\(formatTonUsdValue(Int64(starsValue), divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" + amountRightLabel = "~\(formatTonUsdValue(Int64(starsValue), divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))" } } else { amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.starGiftCommissionStarsPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural)) @@ -458,7 +486,7 @@ private final class SheetContent: CombinedComponent { amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo(tonString).string, attributes: amountMarkdownAttributes, textAlignment: .natural)) if let tonUsdRate = withdrawConfiguration.tonUsdRate { - amountRightLabel = "≈\(formatTonUsdValue(tonValue, divide: true, rate: tonUsdRate, dateTimeFormat: environment.dateTimeFormat))" + amountRightLabel = "~\(formatTonUsdValue(tonValue, divide: true, rate: tonUsdRate, dateTimeFormat: environment.dateTimeFormat))" } } else { amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(environment.strings.Stars_SellGift_AmountInfo("\(resaleConfiguration.starGiftCommissionTonPermille / 10)%").string, attributes: amountMarkdownAttributes, textAlignment: .natural)) @@ -519,6 +547,20 @@ private final class SheetContent: CombinedComponent { maximumNumberOfLines: 0 )) } + case let .starGiftOffer(_, uniqueGift, _): + let giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" + let string: String + switch state.currency { + case .stars: + string = "Enter the number of Stars you'd like to offer for **\(giftTitle)**." + case .ton: + string = "Enter the number of TON you'd like to offer for **\(giftTitle)**." + } + let amountInfoString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(string, attributes: amountMarkdownAttributes, textAlignment: .natural)) + amountFooter = AnyComponent(MultilineTextComponent( + text: .plain(amountInfoString), + maximumNumberOfLines: 0 + )) default: amountFooter = nil } @@ -595,12 +637,14 @@ private final class SheetContent: CombinedComponent { let onlyTonSection = onlyTonSection.update( component: ListSectionComponent( theme: theme, + style: .glass, header: nil, footer: onlyTonFooter, items: [AnyComponentWithIdentity( id: "switch", component: AnyComponent(ListActionItemComponent( theme: theme, + style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( @@ -686,12 +730,14 @@ private final class SheetContent: CombinedComponent { let timestampSection = timestampSection.update( component: ListSectionComponent( theme: theme, + style: .glass, header: nil, footer: timestampFooter, items: [AnyComponentWithIdentity( id: "timestamp", component: AnyComponent(ListActionItemComponent( theme: theme, + style: .glass, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( @@ -743,6 +789,71 @@ private final class SheetContent: CombinedComponent { .cornerRadius(10.0) ) contentSize.height += timestampSection.size.height + } else if case let .starGiftOffer(peer, _, _) = component.mode { + contentSize.height += 24.0 + + let peerName = peer.compactDisplayTitle + + let footerString: String + switch state.currency { + case .stars: + footerString = "Choose how long **\(peerName)** can accept your offer. When the time expires, your Stars will be automatically refunded." + case .ton: + footerString = "Choose how long **\(peerName)** can accept your offer. When the time expires, your TON will be automatically refunded." + } + + let periodFooterString = NSAttributedString(attributedString: parseMarkdownIntoAttributedString(footerString, attributes: amountMarkdownAttributes, textAlignment: .natural)) + let periodFooter = AnyComponent(MultilineTextComponent( + text: .plain(periodFooterString), + maximumNumberOfLines: 0 + )) + + let periodSection = periodSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: periodFooter, + items: [AnyComponentWithIdentity( + id: "timestamp", + component: AnyComponent(ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Offer Duration", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "48 Hours", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )))), + accessory: .expandArrows, + action: { _ in + + } + )) + )] + ), + environment: {}, + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(periodSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: contentSize.height + periodSection.size.height / 2.0)) + .clipsToBounds(true) + .cornerRadius(10.0) + ) + contentSize.height += periodSection.size.height } contentSize.height += 32.0 @@ -789,6 +900,22 @@ private final class SheetContent: CombinedComponent { case .admin: buttonString = environment.strings.Chat_PostSuggestion_Suggest_UpdateButton } + } else if case .starGiftOffer = component.mode { + if let amount = state.amount { + let currencySymbol: String + let currencyAmount: String + switch state.currency { + case .stars: + currencySymbol = "#" + currencyAmount = presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator) + case .ton: + currencySymbol = "$" + currencyAmount = formatTonAmountText(amount.value, dateTimeFormat: environment.dateTimeFormat, maxDecimalPositions: nil) + } + buttonString = "Offer \(currencySymbol) \(currencyAmount)" + } else { + buttonString = "Offer" + } } else if let amount = state.amount { buttonString = "\(environment.strings.Stars_Withdraw_Withdraw) # \(presentationStringsFormattedNumber(amount, environment.dateTimeFormat.groupingSeparator))" } else { @@ -913,8 +1040,9 @@ private final class SheetContent: CombinedComponent { } } } - completion(CurrencyAmount(amount: amount, currency: state.currency), state.timestamp) + case let .starGiftOffer(_, _, completion): + completion(CurrencyAmount(amount: amount, currency: state.currency), state.duration) } controller.dismissAnimated() @@ -952,6 +1080,7 @@ private final class SheetContent: CombinedComponent { fileprivate var amount: StarsAmount? fileprivate var currency: CurrencyAmount.Currency = .stars fileprivate var timestamp: Int32? + fileprivate var duration: Int32 = 120 fileprivate var starsBalance: StarsAmount? private var starsStateDisposable: Disposable? @@ -986,6 +1115,8 @@ private final class SheetContent: CombinedComponent { currency = initialValue.currency amount = initialValue.amount self.timestamp = initialTimestamp + case .starGiftOffer: + amount = nil } self.currency = currency @@ -1178,6 +1309,7 @@ public final class StarsWithdrawScreen: ViewControllerComponentContainer { case starGiftResell(StarGift.UniqueGift, Bool, completion: (CurrencyAmount) -> Void) case paidMessages(current: Int64, minValue: Int64, fractionAfterCommission: Int, kind: StarsWithdrawalScreenSubject.PaidMessageKind, completion: (Int64) -> Void) case suggestedPost(mode: SuggestedPostMode, price: CurrencyAmount, timestamp: Int32?, completion: (CurrencyAmount, Int32?) -> Void) + case starGiftOffer(peer: EnginePeer, gift: StarGift.UniqueGift, completion: (CurrencyAmount, Int32) -> Void) } private let context: AccountContext diff --git a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift index 621e2e4c80..7d93cb30f9 100644 --- a/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift +++ b/submodules/TelegramUI/Components/TabSelectorComponent/Sources/TabSelectorComponent.swift @@ -10,6 +10,11 @@ import AccountContext import TelegramPresentationData public final class TabSelectorComponent: Component { + public enum Style { + case glass + case legacy + } + public final class TransitionHint { public let scrollToEnd: Bool @@ -124,6 +129,7 @@ public final class TabSelectorComponent: Component { public let context: AccountContext? public let colors: Colors public let theme: PresentationTheme + public let style: Style public let customLayout: CustomLayout? public let items: [Item] public let selectedId: AnyHashable? @@ -135,6 +141,7 @@ public final class TabSelectorComponent: Component { context: AccountContext? = nil, colors: Colors, theme: PresentationTheme, + style: Style = .legacy, customLayout: CustomLayout? = nil, items: [Item], selectedId: AnyHashable?, @@ -145,6 +152,7 @@ public final class TabSelectorComponent: Component { self.context = context self.colors = colors self.theme = theme + self.style = style self.customLayout = customLayout self.items = items self.selectedId = selectedId @@ -163,6 +171,9 @@ public final class TabSelectorComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.style != rhs.style { + return false + } if lhs.customLayout != rhs.customLayout { return false } @@ -519,7 +530,14 @@ public final class TabSelectorComponent: Component { self.reorderRecognizer?.isEnabled = component.reorderItem != nil - let baseHeight: CGFloat = 28.0 + let baseHeight: CGFloat + switch component.style { + case .glass: + baseHeight = 32.0 + case .legacy: + baseHeight = 28.0 + } + var verticalInset: CGFloat = 0.0 if let customLayout = component.customLayout { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 96c927354b..81195abb78 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1131,7 +1131,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState let sendGiftTitle: String var isIncoming = message.effectivelyIncoming(context.account.peerId) for media in message.media { - if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action { + if let action = media as? TelegramMediaAction, case let .starGiftUnique(_, isUpgrade, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = action.action { if isUpgrade && message.author?.id == context.account.peerId { isIncoming = true } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 716f336389..0a6d10e448 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3805,6 +3805,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mode = .suggestedPost(mode: .sender(channel: channel, isFromAdmin: isFromAdmin), price: current, timestamp: timestamp, completion: completion) case let .postSuggestionModification(current, timestamp, completion): mode = .suggestedPost(mode: .admin, price: current, timestamp: timestamp, completion: completion) + case let .starGiftOffer(peer, gift, completion): + mode = .starGiftOffer(peer: peer, gift: gift, completion: completion) } return StarsWithdrawScreen(context: context, mode: mode) } From 89f96e28b3c4a36177b50dd0d2978cfb66b6d85c Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 1 Dec 2025 22:18:17 +0800 Subject: [PATCH 02/11] Revert "[WIP] Tab bar and search" This reverts commit 5e26b246bd15fc055ff7698a5783dac76b3e883a. --- Telegram/BUILD | 3 + .../Sources/CallListController.swift | 2 - submodules/ChatListUI/BUILD | 1 - .../Sources/ChatListController.swift | 136 ++-- .../Sources/ChatListControllerNode.swift | 15 +- .../ChatListFilterTabContainerNode.swift | 55 +- .../Sources/Node/ChatListItem.swift | 20 +- .../Source/Base/Transition.swift | 35 - .../Sources/ContactsController.swift | 2 - .../Sources/ContactsControllerNode.swift | 3 +- .../Display/Source/ViewController.swift | 25 - .../SearchBarNode/Sources/SearchBarNode.swift | 91 +-- .../Sources/SearchDisplayController.swift | 189 ++---- .../Sources/TabBarContollerNode.swift | 91 +-- .../TabBarUI/Sources/TabBarController.swift | 37 +- .../Sources/ChatTextInputPanelNode.swift | 3 - .../ChatListFilterTabContainerNode/BUILD | 26 - .../Components/ChatListHeaderComponent/BUILD | 2 - .../Sources/ChatListHeaderComponent.swift | 627 ++++++++++++------ .../Sources/ChatListNavigationBar.swift | 182 +++-- .../Sources/NavigationButtonComponent.swift | 254 ------- .../NavigationButtonContainerView.swift | 6 - .../Sources/ChatListTitleProxyNode.swift | 18 +- .../LiquidLens/Sources/LiquidLensView.swift | 79 +-- .../Sources/MiniAppListScreen.swift | 5 +- .../Sources/PeerInfoScreen.swift | 10 +- .../Components/PeerSelectionController/BUILD | 1 - .../Sources/PeerSelectionController.swift | 1 - .../Sources/QuickReplySetupScreen.swift | 5 +- .../Sources/PeerSelectionScreen.swift | 5 +- .../Components/TabBarComponent/BUILD | 1 - .../Sources/TabBarComponent.swift | 348 ++-------- .../Sources/TelegramRootController.swift | 2 +- 33 files changed, 811 insertions(+), 1469 deletions(-) rename submodules/{TelegramUI/Components/ChatList/ChatListFilterTabContainerNode => ChatListUI}/Sources/ChatListFilterTabContainerNode.swift (96%) delete mode 100644 submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD delete mode 100644 submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift delete mode 100644 submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift diff --git a/Telegram/BUILD b/Telegram/BUILD index a7e40651fc..aacae96803 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -8,6 +8,8 @@ load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application", "ios_extension", "ios_framework", + "ios_unit_test", + "ios_ui_test", ) load("@build_bazel_rules_apple//apple:resources.bzl", @@ -20,6 +22,7 @@ load("@build_bazel_rules_swift//swift:swift.bzl", load( "@rules_xcodeproj//xcodeproj:defs.bzl", + "top_level_target", "top_level_targets", "xcodeproj", "xcode_provisioning_profile", diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index 06bfd50c46..b8e54aecd3 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -155,8 +155,6 @@ public final class CallListController: TelegramBaseController { if case .navigation = self.mode { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } - - self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 47492d7859..a28100bbf7 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -118,7 +118,6 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/EdgeEffect", - "//submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 078854b0e6..5c5e8f23e3 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -55,8 +55,6 @@ import TextFormat import AvatarUploadToastScreen import AdsInfoScreen import AdsReportScreen -import SearchBarNode -import ChatListFilterTabContainerNode private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController @@ -770,8 +768,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) self.updateNavigationMetadata() - - self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { @@ -2842,9 +2838,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { var primaryContent: ChatListHeaderComponent.Content? if let primaryContext = self.primaryContext { - var displayBackButton: Bool = false - if self.previousItem != nil { - displayBackButton = true + var backTitle: String? + if let previousItem = self.previousItem { + switch previousItem { + case let .item(item): + backTitle = item.title ?? self.presentationData.strings.Common_Back + case .close: + backTitle = self.presentationData.strings.Common_Close + } } var navigationBackTitle: String? if case .chatList(.archive) = self.location { @@ -2857,7 +2858,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatListTitle: primaryContext.chatListTitle, leftButton: primaryContext.leftButton, rightButtons: primaryContext.rightButtons, - backPressed: displayBackButton ? { [weak self] in + backTitle: backTitle, + backPressed: backTitle != nil ? { [weak self] in guard let self else { return } @@ -2874,6 +2876,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatListTitle: secondaryContext.chatListTitle, leftButton: secondaryContext.leftButton, rightButtons: secondaryContext.rightButtons, + backTitle: nil, backPressed: { [weak self] in guard let self else { return @@ -4635,7 +4638,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController searchContentNode = navigationBarView.searchContentNode } - self.activateSearch(filter: filter, query: query, skipScrolling: false, searchContentNode: searchContentNode) + if let searchContentNode { + self.activateSearch(filter: filter, query: query, skipScrolling: false, searchContentNode: searchContentNode) + } } public func activateSearch(query: String? = nil) { @@ -4649,37 +4654,45 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private var previousSearchToggleTimestamp: Double? - func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false, searchContentNode: NavigationBarSearchContentNode?) { - Task { @MainActor [weak self] in - guard let self else { + func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false, searchContentNode: NavigationBarSearchContentNode) { + let currentTimestamp = CACurrentMediaTime() + if let previousSearchActivationTimestamp = self.previousSearchToggleTimestamp, currentTimestamp < previousSearchActivationTimestamp + 0.6 { + return + } + self.previousSearchToggleTimestamp = currentTimestamp + + if let storyTooltip = self.storyTooltip { + storyTooltip.dismiss() + } + + var filter = filter + if case .forum = self.chatListDisplayNode.effectiveContainerNode.location { + filter = .topics + } + + if self.chatListDisplayNode.searchDisplayController == nil { + /*if !skipScrolling, let searchContentNode = self.searchContentNode, searchContentNode.expansionProgress != 1.0 { + self.scrollToTop?() + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in + self?.activateSearch(filter: filter, query: query, skipScrolling: true) + }) return - } - - let currentTimestamp = CACurrentMediaTime() - if let previousSearchActivationTimestamp = self.previousSearchToggleTimestamp, currentTimestamp < previousSearchActivationTimestamp + 0.6 { - return - } - self.previousSearchToggleTimestamp = currentTimestamp + }*/ + //TODO:scroll to top? - if let storyTooltip = self.storyTooltip { - storyTooltip.dismiss() - } - - var filter = filter - if case .forum = self.chatListDisplayNode.effectiveContainerNode.location { - filter = .topics - } - - if self.chatListDisplayNode.searchDisplayController == nil { - let (_, _) = await combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)).get() - - do { + let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)) + |> deliverOnMainQueue).startStandalone(next: { [weak self] _, chatListView in + Task { @MainActor in + guard let strongSelf = self else { + return + } + /*if let scrollToTop = strongSelf.scrollToTop { - scrollToTop() - }*/ + scrollToTop() + }*/ let tabsIsEmpty: Bool - if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData { + if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData { tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom } else { tabsIsEmpty = true @@ -4689,43 +4702,40 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let displaySearchFilters = true - if let filterContainerNodeAndActivate = await self.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode?.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: self.hasDownloads, initialFilter: filter, navigationController: self.navigationController as? NavigationController, searchBarIsExternal: true) { + if let filterContainerNodeAndActivate = await strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) { let (filterContainerNode, activate) = filterContainerNodeAndActivate if displaySearchFilters { let searchTabsNode = SparseNode() - self.searchTabsNode = searchTabsNode + strongSelf.searchTabsNode = searchTabsNode searchTabsNode.addSubnode(filterContainerNode) } activate(filter != .downloads) - if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { searchContentNode.search(filter: filter, query: query) } } let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - self.setDisplayNavigationBar(false, transition: transition) - self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: true), transition: transition) - if let searchBarNode = self.currentTabBarSearchNode?() as? SearchBarNode { - self.chatListDisplayNode.searchDisplayController?.setSearchBar(searchBarNode) - searchBarNode.activate() - } - - self.isSearchActive = true - if let navigationController = self.navigationController as? NavigationController { - for controller in navigationController.globalOverlayControllers { - if let controller = controller as? VoiceChatOverlayController { - controller.updateVisibility() - break - } - } + strongSelf.setDisplayNavigationBar(false, transition: transition) + + (strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring)) + } + }) + + self.isSearchActive = true + if let navigationController = self.navigationController as? NavigationController { + for controller in navigationController.globalOverlayControllers { + if let controller = controller as? VoiceChatOverlayController { + controller.updateVisibility() + break } } - } else if self.isSearchActive { - if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { - searchContentNode.search(filter: filter, query: query) - } + } + } else if self.isSearchActive { + if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + searchContentNode.search(filter: filter, query: query) } } } @@ -4756,8 +4766,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated) searchContentNode.placeholderNode.frame = previousFrame - } else { - completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: nil, animated: animated) } self.chatListDisplayNode.tempAllowAvatarExpansion = true @@ -4772,7 +4780,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController completion?() - self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: transition) + (self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring)) self.isSearchActive = false if let navigationController = self.navigationController as? NavigationController { @@ -6228,14 +6236,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } - - override public func tabBarActivateSearch() { - self.activateSearch() - } - - override public func tabBarDeactivateSearch() { - self.deactivateSearch(animated: true) - } private var playedSignUpCompletedAnimation = false public func playSignUpCompletedAnimation() { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 9b4a72354e..3a8357317e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -22,7 +22,6 @@ import ChatListHeaderComponent import StoryPeerListComponent import TelegramNotices import EdgeEffect -import ChatListFilterTabContainerNode public enum ChatListContainerNodeFilter: Equatable { case all @@ -1383,8 +1382,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, - search: nil, //ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, primaryContent: headerContent?.primaryContent, secondaryContent: headerContent?.secondaryContent, secondaryTransition: self.inlineStackContainerTransitionFraction, @@ -1541,8 +1540,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var storiesInset = storiesInset let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: ComponentTransition(transition)) - //self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset - self.mainContainerNode.initialScrollingOffset = navigationBarLayout.storiesInset + self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset navigationBarHeight = navigationBarLayout.navigationHeight visualNavigationHeight = navigationBarLayout.navigationHeight @@ -1668,7 +1666,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } @MainActor - func activateSearch(placeholderNode: SearchBarPlaceholderNode?, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?, searchBarIsExternal: Bool) async -> (ASDisplayNode, (Bool) -> Void)? { + func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) async -> (ASDisplayNode, (Bool) -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else { return nil } @@ -1714,7 +1712,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() } - }, searchBarIsExternal: searchBarIsExternal) + }) self.mainContainerNode.accessibilityElementsHidden = true self.inlineStackContainerNode?.accessibilityElementsHidden = true @@ -1744,7 +1742,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { }) } - func deactivateSearch(placeholderNode: SearchBarPlaceholderNode?, animated: Bool) -> (() -> Void)? { + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) -> (() -> Void)? { if let searchDisplayController = self.searchDisplayController { self.isSearchDisplayControllerActive = false self.searchDisplayController = nil @@ -1752,8 +1750,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.inlineStackContainerNode?.accessibilityElementsHidden = false return { [weak self, weak placeholderNode] in - if let strongSelf = self, let (layout, _, _, cleanNavigationBarHeight, _) = strongSelf.containerLayout { - let placeholderNode = placeholderNode + if let strongSelf = self, let placeholderNode, let (layout, _, _, cleanNavigationBarHeight, _) = strongSelf.containerLayout { searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated) searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) diff --git a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift similarity index 96% rename from submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift rename to submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift index 9e3fb4dd03..b20fc85916 100644 --- a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift @@ -210,7 +210,7 @@ private final class ItemNode: ASDisplayNode { if self.theme !== presentationData.theme { self.theme = presentationData.theme - self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chat.inputPanel.panelControlColor) + self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeActiveBackgroundColor) self.badgeBackgroundInactiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor) themeUpdated = true @@ -299,10 +299,10 @@ private final class ItemNode: ASDisplayNode { if themeUpdated || titleUpdated { self.titleNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.chat.inputPanel.panelControlColor) + self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) self.shortTitleNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.chat.inputPanel.panelControlColor) + self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) } if unreadCount != 0 { @@ -438,8 +438,8 @@ public enum ChatListFilterTabEntryId: Hashable { } public struct ChatListFilterTabEntryUnreadCount: Equatable { - public let value: Int - public let hasUnmuted: Bool + let value: Int + let hasUnmuted: Bool public init(value: Int, hasUnmuted: Bool) { self.value = value @@ -482,14 +482,14 @@ public enum ChatListFilterTabEntry: Equatable { public final class ChatListFilterTabContainerNode: ASDisplayNode { private let context: AccountContext private let scrollNode: ASScrollNode - private let selectedBackgroundNode: ASImageNode + private let selectedLineNode: ASImageNode private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:] public var tabSelected: ((ChatListFilterTabEntryId, Bool) -> Void)? - public var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? - public var addFilter: (() -> Void)? - public var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? - public var presentPremiumTip: (() -> Void)? + var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? + var addFilter: (() -> Void)? + var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? + var presentPremiumTip: (() -> Void)? private var reorderingGesture: ReorderingGestureRecognizer? private var reorderingItem: ChatListFilterTabEntryId? @@ -501,7 +501,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, canReorderAllChats: Bool, filtersLimit: Int32?, transitionFraction: CGFloat, presentationData: PresentationData)? - public var reorderedFilterIds: [Int32]? { + var reorderedFilterIds: [Int32]? { return self.reorderedItemIds.flatMap { $0.compactMap { switch $0 { @@ -533,9 +533,9 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.context = context self.scrollNode = ASScrollNode() - self.selectedBackgroundNode = ASImageNode() - self.selectedBackgroundNode.displaysAsynchronously = false - self.selectedBackgroundNode.displayWithoutProcessing = true + self.selectedLineNode = ASImageNode() + self.selectedLineNode.displaysAsynchronously = false + self.selectedLineNode.displayWithoutProcessing = true super.init() @@ -549,7 +549,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } self.addSubnode(self.scrollNode) - self.scrollNode.addSubnode(self.selectedBackgroundNode) + self.scrollNode.addSubnode(self.selectedLineNode) let reorderingGesture = ReorderingGestureRecognizer(shouldBegin: { [weak self] point in guard let strongSelf = self else { @@ -677,7 +677,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { private var previousSelectedFrame: CGRect? public func cancelAnimations() { - self.selectedBackgroundNode.layer.removeAllAnimations() + self.selectedLineNode.layer.removeAllAnimations() self.scrollNode.layer.removeAllAnimations() } @@ -695,7 +695,14 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { let previousContentWidth = self.scrollNode.view.contentSize.width if self.currentParams?.presentationData.theme !== presentationData.theme { - self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: presentationData.theme.chatList.itemHighlightedBackgroundColor) + self.selectedLineNode.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 4.0, height: 4.0))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: 0.0), size: CGSize(width: 4.0, height: 4.0))) + context.fill(CGRect(x: 2.0, y: 0.0, width: size.width - 4.0, height: 4.0)) + context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0)) + })?.resizableImage(withCapInsets: UIEdgeInsets(top: 3.0, left: 3.0, bottom: 0.0, right: 3.0), resizingMode: .stretch) } if isReordering { @@ -857,7 +864,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { let minSpacing: CGFloat = 26.0 - let resolvedSideInset: CGFloat = 25.0 + sideInset + let resolvedSideInset: CGFloat = 16.0 + sideInset var leftOffset: CGFloat = resolvedSideInset var longTitlesWidth: CGFloat = resolvedSideInset @@ -930,13 +937,13 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } if let selectedFrame = selectedFrame { - let wasAdded = self.selectedBackgroundNode.isHidden - self.selectedBackgroundNode.isHidden = false - let selectedBackgroundFrame = CGRect(origin: CGPoint(x: selectedFrame.minX - 10.0, y: selectedFrame.minY - floor((36.0 - selectedFrame.height) * 0.5)), size: CGSize(width: selectedFrame.width + 10.0 * 2.0, height: 36.0)) + let wasAdded = self.selectedLineNode.isHidden + self.selectedLineNode.isHidden = false + let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 3.0), size: CGSize(width: selectedFrame.width, height: 3.0)) if wasAdded { - self.selectedBackgroundNode.frame = selectedBackgroundFrame + self.selectedLineNode.frame = lineFrame } else { - transition.updateFrame(node: self.selectedBackgroundNode, frame: selectedBackgroundFrame) + transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) } if let previousSelectedFrame = self.previousSelectedFrame { @@ -965,7 +972,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) self.previousSelectedFrame = selectedFrame } else { - self.selectedBackgroundNode.isHidden = true + self.selectedLineNode.isHidden = true self.previousSelectedAbsFrame = nil self.previousSelectedFrame = nil } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 9b445ccc3f..46974fd940 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2103,9 +2103,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let effectiveBackgroundColor: UIColor if item.isPinned { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat) - //TODO:localize - effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor - //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat) effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor @@ -2119,9 +2117,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let avatarLiveBadge = self.avatarLiveBadge { let effectiveBackgroundColor: UIColor if item.isPinned { - //TODO:localize - effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor - //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } @@ -4131,9 +4127,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let effectiveBackgroundColor: UIColor if item.isPinned { - //TODO:localize - effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor - //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } @@ -4154,9 +4148,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { effectiveBackgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor } else if case let .chatList(index) = item.index, index.pinningIndex != nil { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat) - //TODO:localize - effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor - //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat) effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor @@ -5068,9 +5060,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { backgroundColor = theme.itemBackgroundColor highlightedBackgroundColor = theme.itemHighlightedBackgroundColor } else { - //TODO:localize - backgroundColor = item.presentationData.theme.chatList.itemBackgroundColor - //backgroundColor = theme.pinnedItemBackgroundColor + backgroundColor = theme.pinnedItemBackgroundColor highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor } } else { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index ae393778c4..4dcd0401d2 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1335,41 +1335,6 @@ public struct ComponentTransition { completion: completion ) } - - public func setBlur(layer: CALayer, radius: CGFloat, completion: ((Bool) -> Void)? = nil) { - var currentRadius: CGFloat = 0.0 - if let currentFilters = layer.filters { - for filter in currentFilters { - if let filter = filter as? NSObject, filter.description.contains("gaussianBlur") { - currentRadius = filter.value(forKey: "inputRadius") as? CGFloat ?? 0.0 - } - } - } - - if currentRadius == radius { - completion?(true) - return - } - - if let blurFilter = CALayer.blur() { - blurFilter.setValue(radius as NSNumber, forKey: "inputRadius") - layer.filters = [blurFilter] - switch self.animation { - case .none: - completion?(true) - case let .curve(duration, curve): - layer.animate(from: currentRadius as NSNumber, to: radius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", duration: duration, delay: 0.0, curve: curve, removeOnCompletion: true, additive: false,completion: { [weak layer] flag in - if let layer { - if radius <= 0.0 { - layer.filters = nil - } - } - - completion?(flag) - }) - } - } - } public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { let duration: Double diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index e75897f583..425ca2b7ab 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -226,8 +226,6 @@ public class ContactsController: ViewController { } self.sortButton.addTarget(self, action: #selector(self.sortPressed), forControlEvents: .touchUpInside) - - self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index bd1e967a3b..2718f81f91 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -350,6 +350,7 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { chatListTitle: NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil), leftButton: leftButton, rightButtons: rightButtons, + backTitle: nil, backPressed: nil ) @@ -361,8 +362,8 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, - search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, primaryContent: primaryContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index f81403f2dd..593005a7e5 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -217,18 +217,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open var navigationBarRequiresEntireLayoutUpdate: Bool { return true } - - public struct TabBarSearchState: Equatable { - public var isActive: Bool - - public init(isActive: Bool) { - self.isActive = isActive - } - } - - public private(set) var tabBarSearchState: TabBarSearchState? - public var tabBarSearchStateUpdated: ((ContainedViewLayoutTransition) -> Void)? - public var currentTabBarSearchNode: (() -> ASDisplayNode?)? private weak var activeInputViewCandidate: UIResponder? private weak var activeInputView: UIResponder? @@ -717,22 +705,9 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open func tabBarDisabledAction() { } - - open func tabBarActivateSearch() { - } - - open func tabBarDeactivateSearch() { - } open func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) { } - - public func updateTabBarSearchState(_ tabBarSearchState: TabBarSearchState?, transition: ContainedViewLayoutTransition) { - if self.tabBarSearchState != tabBarSearchState { - self.tabBarSearchState = tabBarSearchState - self.tabBarSearchStateUpdated?(transition) - } - } open func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) { } diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 773e1f4259..131323d318 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -369,7 +369,6 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { } var theme: SearchBarNodeTheme - let style: SearchBarStyle fileprivate func layoutTokens(transition: ContainedViewLayoutTransition = .immediate) { var hasSelected = false @@ -520,9 +519,8 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { } } - init(theme: SearchBarNodeTheme, style: SearchBarStyle) { + init(theme: SearchBarNodeTheme) { self.theme = theme - self.style = style self.placeholderLabel = ImmediateTextNode() self.placeholderLabel.isUserInteractionEnabled = false @@ -681,10 +679,6 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { placeholderYOffset = 1.0 } else { } - - if case .inlineNavigation = self.style { - placeholderYOffset += 6.0 - } let textRect = self.textRect(forBounds: bounds) let labelSize = self.placeholderLabel.updateLayout(textRect.size) @@ -819,45 +813,40 @@ public final class SearchBarNodeTheme: Equatable { public enum SearchBarStyle { case modern case legacy - case inlineNavigation var font: UIFont { switch self { - case .modern, .inlineNavigation: - return Font.regular(17.0) - case .legacy: - return Font.regular(14.0) + case .modern: + return Font.regular(17.0) + case .legacy: + return Font.regular(14.0) } } var cornerDiameter: CGFloat { switch self { - case .modern, .inlineNavigation: - return 21.0 - case .legacy: - return 14.0 + case .modern: + return 21.0 + case .legacy: + return 14.0 } } var height: CGFloat { switch self { - case .inlineNavigation: - return 48.0 - case .modern: - return 36.0 - case .legacy: - return 28.0 + case .modern: + return 36.0 + case .legacy: + return 28.0 } } var padding: CGFloat { switch self { - case .inlineNavigation: - return 0.0 - case .modern: - return 10.0 - case .legacy: - return 8.0 + case .modern: + return 10.0 + case .legacy: + return 8.0 } } } @@ -1005,7 +994,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true - self.textField = SearchBarTextField(theme: theme, style: fieldStyle) + self.textField = SearchBarTextField(theme: theme) self.textField.accessibilityTraits = .searchField self.textField.autocorrectionType = .no self.textField.returnKeyType = .search @@ -1022,21 +1011,14 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { super.init() - if case .inlineNavigation = self.fieldStyle { - } else { - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - self.addSubnode(self.textBackgroundNode) - } + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + + self.addSubnode(self.textBackgroundNode) self.view.addSubview(self.textField) - - if case .inlineNavigation = self.fieldStyle { - } else { - self.addSubnode(self.iconNode) - self.addSubnode(self.cancelButton) - } - + self.addSubnode(self.iconNode) self.addSubnode(self.clearButton) + self.addSubnode(self.cancelButton) self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) @@ -1111,34 +1093,19 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: .immediate) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))) + let verticalOffset: CGFloat = boundingSize.height - 82.0 + let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height)) - let textBackgroundHeight: CGFloat - if case .inlineNavigation = self.fieldStyle { - textBackgroundHeight = boundingSize.height - } else { - textBackgroundHeight = self.fieldStyle.height - } - let verticalOffset: CGFloat - if case .inlineNavigation = self.fieldStyle { - verticalOffset = -textBackgroundHeight - } else { - verticalOffset = boundingSize.height - 82.0 - } + let textBackgroundHeight = self.fieldStyle.height let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity)) transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 10.0 - cancelButtonSize.width, y: verticalOffset + textBackgroundHeight + floorToScreenPixels((textBackgroundHeight - cancelButtonSize.height) / 2.0)), size: cancelButtonSize)) let padding = self.fieldStyle.padding - var textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) + let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) - var textFrame = CGRect(origin: CGPoint(x: 0.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height)) - if case .inlineNavigation = self.fieldStyle { - textFrame.size.width = boundingSize.width - 27.0 - textBackgroundFrame.size.width = boundingSize.width - } else { - textFrame.origin.x = textBackgroundFrame.minX + 24.0 - } + let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 24.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height)) if let iconImage = self.iconNode.image { let iconSize = iconImage.size diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index 3c885e33e0..50420e6b50 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -25,14 +25,12 @@ public final class SearchDisplayController { } } - private var searchBar: SearchBarNode? - private let searchBarIsExternal: Bool + private let searchBar: SearchBarNode private let mode: SearchDisplayControllerMode private let backgroundNode: BackgroundNode public let contentNode: SearchDisplayControllerContentNode private var hasSeparator: Bool private let inline: Bool - private let cancel: () -> Void private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -40,75 +38,71 @@ public final class SearchDisplayController { private var isSearchingDisposable: Disposable? - public init( - presentationData: PresentationData, - mode: SearchDisplayControllerMode = .navigation, - placeholder: String? = nil, - hasBackground: Bool = false, - hasSeparator: Bool = false, - contentNode: SearchDisplayControllerContentNode, - inline: Bool = false, - cancel: @escaping () -> Void, - fieldStyle: SearchBarStyle = .modern, - searchBarIsExternal: Bool = false - ) { + public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, hasBackground: Bool = false, hasSeparator: Bool = false, contentNode: SearchDisplayControllerContentNode, inline: Bool = false, cancel: @escaping () -> Void) { self.inline = inline - self.cancel = cancel - self.searchBarIsExternal = searchBarIsExternal - - if !searchBarIsExternal { - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator, inline: inline), strings: presentationData.strings, fieldStyle: fieldStyle, forceSeparator: hasSeparator, displayBackground: hasBackground) - } + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator, inline: inline), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator, displayBackground: hasBackground) self.backgroundNode = BackgroundNode() self.backgroundNode.allowsGroupOpacity = true self.mode = mode self.contentNode = contentNode self.hasSeparator = hasSeparator - - if let searchBar = self.searchBar { - self.setSearchBar(searchBar) - } + self.searchBar.textUpdated = { [weak contentNode] text, _ in + contentNode?.searchTextUpdated(text: text) + } + self.searchBar.tokensUpdated = { [weak contentNode] tokens in + contentNode?.searchTokensUpdated(tokens: tokens) + } + self.searchBar.cancel = { [weak self] in + self?.isDeactivating = true + cancel() + } + self.searchBar.clearPrefix = { [weak contentNode] in + contentNode?.searchTextClearPrefix() + } + self.searchBar.clearTokens = { [weak contentNode] in + contentNode?.searchTextClearTokens() + } self.contentNode.cancel = { [weak self] in self?.isDeactivating = true cancel() } self.contentNode.dismissInput = { [weak self] in - self?.searchBar?.deactivate(clear: false) + self?.searchBar.deactivate(clear: false) } var isFirstTime = true self.contentNode.setQuery = { [weak self] prefix, tokens, query in - if let strongSelf = self, let searchBar = strongSelf.searchBar { - searchBar.prefixString = prefix - let previousTokens = searchBar.tokens - searchBar.tokens = tokens - searchBar.text = query + if let strongSelf = self { + strongSelf.searchBar.prefixString = prefix + let previousTokens = strongSelf.searchBar.tokens + strongSelf.searchBar.tokens = tokens + strongSelf.searchBar.text = query if previousTokens.count < tokens.count && !isFirstTime { if let lastToken = tokens.last, !lastToken.permanent { - searchBar.selectLastToken() + strongSelf.searchBar.selectLastToken() } } isFirstTime = false } } if let placeholder = placeholder { - self.searchBar?.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) } self.contentNode.setPlaceholder = { [weak self] string in - guard string != self?.searchBar?.placeholderString?.string else { + guard string != self?.searchBar.placeholderString?.string else { return } - if let mutableAttributedString = self?.searchBar?.placeholderString?.mutableCopy() as? NSMutableAttributedString { + if let mutableAttributedString = self?.searchBar.placeholderString?.mutableCopy() as? NSMutableAttributedString { mutableAttributedString.mutableString.setString(string) - self?.searchBar?.placeholderString = mutableAttributedString + self?.searchBar.placeholderString = mutableAttributedString } } self.isSearchingDisposable = (contentNode.isSearching |> deliverOnMainQueue).start(next: { [weak self] value in - self?.searchBar?.activity = value + self?.searchBar.activity = value }) if self.contentNode.hasDim { @@ -119,32 +113,9 @@ public final class SearchDisplayController { self.backgroundNode.isTransparent = false } } - - public func setSearchBar(_ searchBar: SearchBarNode) { - self.searchBar = searchBar - - searchBar.textUpdated = { [weak contentNode] text, _ in - contentNode?.searchTextUpdated(text: text) - } - searchBar.tokensUpdated = { [weak contentNode] tokens in - contentNode?.searchTokensUpdated(tokens: tokens) - } - searchBar.cancel = { [weak self] in - self?.isDeactivating = true - self?.cancel() - } - searchBar.clearPrefix = { [weak contentNode] in - contentNode?.searchTextClearPrefix() - } - searchBar.clearTokens = { [weak contentNode] in - contentNode?.searchTextClearTokens() - } - } public func updatePresentationData(_ presentationData: PresentationData) { - if !self.searchBarIsExternal { - self.searchBar?.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator, inline: self.inline), strings: presentationData.strings) - } + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator, inline: self.inline), strings: presentationData.strings) self.contentNode.updatePresentationData(presentationData) if self.contentNode.hasDim { @@ -166,27 +137,21 @@ public final class SearchDisplayController { navigationBarOffset = 0.0 } var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight)) - if self.searchBarIsExternal { - navigationBarFrame.size.height -= 10.0 - } if layout.statusBarHeight == nil { navigationBarFrame.size.height = 64.0 } navigationBarFrame.size.height += 10.0 - let navigationBarHeight = navigationBarFrame.maxY - if !self.searchBarIsExternal, let searchBar = self.searchBar { - let searchBarFrame: CGRect - if case .navigation = self.mode { - searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) - } else { - searchBarFrame = navigationBarFrame - } - transition.updateFrame(node: searchBar, frame: searchBarFrame) - searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) + let searchBarFrame: CGRect + if case .navigation = self.mode { + searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) + } else { + searchBarFrame = navigationBarFrame } + transition.updateFrame(node: self.searchBar, frame: searchBarFrame) + self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) - self.containerLayout = (layout, navigationBarHeight) + self.containerLayout = (layout, navigationBarFrame.maxY) let bounds = CGRect(origin: CGPoint(), size: layout.size) transition.updateFrame(node: self.backgroundNode, frame: bounds.insetBy(dx: -20.0, dy: -20.0)) @@ -240,20 +205,18 @@ public final class SearchDisplayController { self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } - if !self.searchBarIsExternal { - if !self.contentNode.hasDim { - if let placeholder = placeholder { - self.searchBar?.placeholderString = placeholder.placeholderString - } - } else { - if let placeholder = placeholder { - let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) - let contentNodePosition = self.backgroundNode.layer.position - if contentNode.animateBackgroundAppearance { - self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - } - self.searchBar?.placeholderString = placeholder.placeholderString + if !self.contentNode.hasDim { + if let placeholder = placeholder { + self.searchBar.placeholderString = placeholder.placeholderString + } + } else { + if let placeholder = placeholder { + let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) + let contentNodePosition = self.backgroundNode.layer.position + if contentNode.animateBackgroundAppearance { + self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } + self.searchBar.placeholderString = placeholder.placeholderString } } @@ -277,49 +240,39 @@ public final class SearchDisplayController { navigationBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) } - if !self.searchBarIsExternal, let searchBar = self.searchBar { - searchBar.frame = navigationBarFrame - insertSubnode(searchBar, true) - searchBar.layout() - - if focus { - searchBar.activate() - } + self.searchBar.frame = navigationBarFrame + insertSubnode(self.searchBar, true) + self.searchBar.layout() + + if focus { + self.searchBar.activate() } - if let placeholder = placeholder { - if !self.searchBarIsExternal, let searchBar = self.searchBar { - searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - } + self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) if self.contentNode.hasDim { self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } else { - if !self.searchBarIsExternal, let searchBar = self.searchBar { - searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) - } + self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) { - if let searchBar = self.searchBar { - searchBar.deactivate(clear: false) - } + self.searchBar.deactivate(clear: false) - if !self.searchBarIsExternal, let searchBar = self.searchBar { - if let placeholder = placeholder { - searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { - [weak searchBar] in + let searchBar = self.searchBar + if let placeholder = placeholder { + searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { + [weak searchBar] in + searchBar?.removeFromSupernode() + }) + } else { + searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] finished in + if finished { searchBar?.removeFromSupernode() - }) - } else { - searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] finished in - if finished { - searchBar?.removeFromSupernode() - } - }) - } + } + }) } let backgroundNode = self.backgroundNode diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index c346769805..abee862105 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -18,18 +18,15 @@ final class TabBarControllerNode: ASDisplayNode { let layout: ContainerViewLayout let toolbar: Toolbar? let isTabBarHidden: Bool - let currentControllerSearchState: ViewController.TabBarSearchState? init( layout: ContainerViewLayout, toolbar: Toolbar?, - isTabBarHidden: Bool, - currentControllerSearchState: ViewController.TabBarSearchState? + isTabBarHidden: Bool ) { self.layout = layout self.toolbar = toolbar self.isTabBarHidden = isTabBarHidden - self.currentControllerSearchState = currentControllerSearchState } } @@ -54,7 +51,6 @@ final class TabBarControllerNode: ASDisplayNode { } private var theme: PresentationTheme - private var strings: PresentationStrings private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void private let contextAction: (Int, ContextExtractedContentContainingView, ContextGesture) -> Void @@ -64,26 +60,24 @@ final class TabBarControllerNode: ASDisplayNode { private var toolbarNode: ToolbarNode? private let toolbarActionSelected: (ToolbarActionOption) -> Void private let disabledPressed: () -> Void - private let activateSearch: () -> Void - private let deactivateSearch: () -> Void private(set) var tabBarItems: [TabBarNodeItem] = [] private(set) var selectedIndex: Int = 0 - private weak var currentController: ViewController? + private(set) var currentControllerNode: ASDisplayNode? private var layoutResult: LayoutResult? private var isUpdateRequested: Bool = false private var isChangingSelectedIndex: Bool = false - func setCurrentController(_ controller: ViewController?) -> () -> Void { - guard controller !== self.currentController else { + func setCurrentControllerNode(_ node: ASDisplayNode?) -> () -> Void { + guard node !== self.currentControllerNode else { return {} } - let previousNode = self.currentController?.displayNode - self.currentController = controller - if let currentControllerNode = self.currentController?.displayNode { + let previousNode = self.currentControllerNode + self.currentControllerNode = node + if let currentControllerNode = self.currentControllerNode { if let previousNode { self.insertSubnode(currentControllerNode, aboveSubnode: previousNode) } else { @@ -95,22 +89,14 @@ final class TabBarControllerNode: ASDisplayNode { } return { [weak self, weak previousNode] in - if previousNode !== self?.currentController?.displayNode { + if previousNode !== self?.currentControllerNode { previousNode?.removeFromSupernode() } } } - - var currentSearchNode: ASDisplayNode? { - if let tabBarComponentView = self.tabBarView.view as? TabBarComponent.View { - return tabBarComponentView.currentSearchNode - } - return nil - } - init(theme: PresentationTheme, strings: PresentationStrings, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void, activateSearch: @escaping () -> Void, deactivateSearch: @escaping () -> Void) { + init(theme: PresentationTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { self.theme = theme - self.strings = strings self.itemSelected = itemSelected self.contextAction = contextAction self.disabledOverlayNode = ASDisplayNode() @@ -118,9 +104,7 @@ final class TabBarControllerNode: ASDisplayNode { self.disabledOverlayNode.alpha = 0.0 self.toolbarActionSelected = toolbarActionSelected self.disabledPressed = disabledPressed - self.activateSearch = activateSearch - self.deactivateSearch = deactivateSearch - + super.init() self.setViewBlock({ @@ -179,7 +163,7 @@ final class TabBarControllerNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) -> CGFloat { - let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden, currentControllerSearchState: self.currentController?.tabBarSearchState) + let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden) if let layoutResult = self.layoutResult, layoutResult.params == params { return layoutResult.bottomInset } else { @@ -195,27 +179,18 @@ final class TabBarControllerNode: ASDisplayNode { } private func updateImpl(params: Params, transition: ContainedViewLayoutTransition) -> CGFloat { - var panelsBottomInset: CGFloat = params.layout.insets(options: []).bottom - if params.layout.metrics.widthClass == .regular, let inputHeight = params.layout.inputHeight, inputHeight != 0.0 { - panelsBottomInset = inputHeight + 8.0 + var options: ContainerViewLayoutInsetOptions = [] + if params.layout.metrics.widthClass == .regular { + options.insert(.input) } - if panelsBottomInset == 0.0 { - panelsBottomInset = 8.0 + + var bottomInset: CGFloat = params.layout.insets(options: options).bottom + if bottomInset == 0.0 { + bottomInset = 8.0 } else { - panelsBottomInset = max(panelsBottomInset, 8.0) - } - - var tabBarBottomInset: CGFloat = panelsBottomInset - if let currentController = self.currentController { - if let tabBarSearchState = currentController.tabBarSearchState, tabBarSearchState.isActive, let inputHeight = params.layout.inputHeight, inputHeight != 0.0 { - tabBarBottomInset = max(tabBarBottomInset, inputHeight + 8.0) - } - } - - var sideInset: CGFloat = 12.0 - if tabBarBottomInset <= 28.0 { - sideInset = 20.0 + bottomInset = max(bottomInset, 8.0) } + let sideInset: CGFloat = 20.0 var selectedId: AnyHashable? if self.selectedIndex < self.tabBarItems.count { @@ -233,7 +208,6 @@ final class TabBarControllerNode: ASDisplayNode { transition: tabBarTransition, component: AnyComponent(TabBarComponent( theme: self.theme, - strings: self.strings, items: self.tabBarItems.map { item in let itemId = AnyHashable(ObjectIdentifier(item.item)) return TabBarComponent.Item( @@ -256,30 +230,13 @@ final class TabBarControllerNode: ASDisplayNode { } ) }, - search: self.currentController?.tabBarSearchState.flatMap { tabBarSearchState in - return TabBarComponent.Search( - isActive: tabBarSearchState.isActive, - activate: { [weak self] in - guard let self else { - return - } - self.activateSearch() - }, - deactivate: { [weak self] in - guard let self else { - return - } - self.deactivateSearch() - } - ) - }, selectedId: selectedId, isTablet: params.layout.metrics.isTablet )), environment: {}, containerSize: CGSize(width: params.layout.size.width - sideInset * 2.0, height: 100.0) ) - let tabBarFrame = CGRect(origin: CGPoint(x: floor((params.layout.size.width - tabBarSize.width) * 0.5), y: params.layout.size.height - (self.tabBarHidden ? 0.0 : (tabBarSize.height + tabBarBottomInset))), size: tabBarSize) + let tabBarFrame = CGRect(origin: CGPoint(x: floor((params.layout.size.width - tabBarSize.width) * 0.5), y: params.layout.size.height - (self.tabBarHidden ? 0.0 : (tabBarSize.height + bottomInset))), size: tabBarSize) if let tabBarComponentView = self.tabBarView.view { if tabBarComponentView.superview == nil { @@ -291,13 +248,13 @@ final class TabBarControllerNode: ASDisplayNode { transition.updateFrame(node: self.disabledOverlayNode, frame: tabBarFrame) - let toolbarHeight = 50.0 + panelsBottomInset + let toolbarHeight = 50.0 + params.layout.insets(options: options).bottom let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: params.layout.size.height - toolbarHeight), size: CGSize(width: params.layout.size.width, height: toolbarHeight)) if let toolbar = params.toolbar { if let toolbarNode = self.toolbarNode { transition.updateFrame(node: toolbarNode, frame: toolbarFrame) - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: panelsBottomInset, toolbar: toolbar, transition: transition) + toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition) } else { let toolbarNode = ToolbarNode(theme: ToolbarTheme(theme: self.theme), displaySeparator: true, left: { [weak self] in self?.toolbarActionSelected(.left) @@ -307,7 +264,7 @@ final class TabBarControllerNode: ASDisplayNode { self?.toolbarActionSelected(.middle) }) toolbarNode.frame = toolbarFrame - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: panelsBottomInset, toolbar: toolbar, transition: .immediate) + toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate) self.addSubnode(toolbarNode) self.toolbarNode = toolbarNode if transition.isAnimated { diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index 111fe208d1..a3f7fe0806 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -93,11 +93,9 @@ open class TabBarControllerImpl: ViewController, TabBarController { private let pendingControllerDisposable = MetaDisposable() private var theme: PresentationTheme - private var strings: PresentationStrings - public init(theme: PresentationTheme, strings: PresentationStrings) { + public init(theme: PresentationTheme) { self.theme = theme - self.strings = strings super.init(navigationBarPresentationData: nil) @@ -154,7 +152,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { } override open func loadDisplayNode() { - self.displayNode = TabBarControllerNode(theme: self.theme, strings: self.strings, itemSelected: { [weak self] index, longTap, itemNodes in + self.displayNode = TabBarControllerNode(theme: self.theme, itemSelected: { [weak self] index, longTap, itemNodes in if let strongSelf = self { if longTap, let controller = strongSelf.controllers[index] as? TabBarContainedController { controller.presentTabBarPreviewingController(sourceNodes: itemNodes) @@ -237,16 +235,6 @@ open class TabBarControllerImpl: ViewController, TabBarController { self?.currentController?.toolbarActionSelected(action: action) }, disabledPressed: { [weak self] in self?.currentController?.tabBarDisabledAction() - }, activateSearch: { [weak self] in - guard let self else { - return - } - self.currentController?.tabBarActivateSearch() - }, deactivateSearch: { [weak self] in - guard let self else { - return - } - self.currentController?.tabBarDeactivateSearch() }) self.updateSelectedIndex() @@ -275,8 +263,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { } if let currentController = self.currentController { currentController.willMove(toParent: nil) - currentController.tabBarSearchStateUpdated = nil - currentController.currentTabBarSearchNode = nil + //self.tabBarControllerNode.currentControllerNode = nil if animated { currentController.view.layer.animateScale(from: 1.0, to: transitionScale, duration: 0.12, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { completed in @@ -299,7 +286,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { currentController.willMove(toParent: self) self.addChild(currentController) - let commit = self.tabBarControllerNode.setCurrentController(currentController) + let commit = self.tabBarControllerNode.setCurrentControllerNode(currentController.displayNode) if animated { currentController.view.layer.animateScale(from: transitionScale, to: 1.0, duration: 0.15, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring) currentController.view.layer.allowsGroupOpacity = true @@ -316,22 +303,6 @@ open class TabBarControllerImpl: ViewController, TabBarController { currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle - - currentController.tabBarSearchStateUpdated = { [weak self] transition in - guard let self else { - return - } - if let layout = self.validLayout { - self.containerLayoutUpdated(layout, transition: transition) - } - } - - currentController.currentTabBarSearchNode = { [weak self] in - guard let self else { - return nil - } - return self.tabBarControllerNode.currentSearchNode - } } if let layout = self.validLayout { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 99ef8a56b5..903663967c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -1174,9 +1174,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.touchDownGestureRecognizer = recognizer textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string - - self.isAccessibilityContainer = true - self.accessibilityElements = [textInputNode.textView] } private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics, bottomInset: CGFloat) -> CGFloat { diff --git a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD deleted file mode 100644 index 4805b62a3d..0000000000 --- a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD +++ /dev/null @@ -1,26 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "ChatListFilterTabContainerNode", - module_name = "ChatListFilterTabContainerNode", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/AsyncDisplayKit", - "//submodules/Display", - "//submodules/TelegramCore", - "//submodules/TelegramPresentationData", - "//submodules/TelegramUI/Components/TextNodeWithEntities", - "//submodules/AccountContext", - "//submodules/ComponentFlow", - "//submodules/TelegramUI/Components/GlassBackgroundComponent", - "//submodules/TelegramUI/Components/LiquidLens", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD index 5355cc429f..6f94cb0c07 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD @@ -24,8 +24,6 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters", "//submodules/SearchUI", "//submodules/TelegramUI/Components/MoreHeaderButton", - "//submodules/TelegramUI/Components/EdgeEffect", - "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 571c5cfe77..8e235da5ea 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -9,7 +9,6 @@ import AppBundle import StoryPeerListComponent import TelegramCore import MoreHeaderButton -import GlassBackgroundComponent public final class HeaderNetworkStatusComponent: Component { public enum Content: Equatable { @@ -80,6 +79,7 @@ public final class ChatListHeaderComponent: Component { public let chatListTitle: NetworkStatusTitle? public let leftButton: AnyComponentWithIdentity? public let rightButtons: [AnyComponentWithIdentity] + public let backTitle: String? public let backPressed: (() -> Void)? public init( @@ -89,6 +89,7 @@ public final class ChatListHeaderComponent: Component { chatListTitle: NetworkStatusTitle?, leftButton: AnyComponentWithIdentity?, rightButtons: [AnyComponentWithIdentity], + backTitle: String?, backPressed: (() -> Void)? ) { self.title = title @@ -97,6 +98,7 @@ public final class ChatListHeaderComponent: Component { self.chatListTitle = chatListTitle self.leftButton = leftButton self.rightButtons = rightButtons + self.backTitle = backTitle self.backPressed = backPressed } @@ -119,7 +121,7 @@ public final class ChatListHeaderComponent: Component { if lhs.rightButtons != rhs.rightButtons { return false } - if (lhs.backPressed == nil) != (rhs.backPressed == nil) { + if lhs.backTitle != rhs.backTitle { return false } return true @@ -224,6 +226,8 @@ public final class ChatListHeaderComponent: Component { private let onPressed: () -> Void let arrowView: UIImageView + let titleOffsetContainer: UIView + let titleView: ImmediateTextView private var currentColor: UIColor? @@ -231,11 +235,16 @@ public final class ChatListHeaderComponent: Component { self.onPressed = onPressed self.arrowView = UIImageView() + self.titleOffsetContainer = UIView() + self.titleView = ImmediateTextView() super.init(frame: CGRect()) self.addSubview(self.arrowView) + self.addSubview(self.titleOffsetContainer) + self.titleOffsetContainer.addSubview(self.titleView) + self.highligthedChanged = { [weak self] highlighted in guard let self else { return @@ -257,37 +266,37 @@ public final class ChatListHeaderComponent: Component { self.onPressed() } - func update(theme: PresentationTheme, strings: PresentationStrings, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - self.accessibilityLabel = strings.Common_Back + func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleTextUpdated = self.titleView.attributedText != titleText + self.titleView.attributedText = titleText + let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) + + self.accessibilityLabel = title self.accessibilityTraits = [.button] - if self.currentColor != theme.chat.inputPanel.panelControlColor { - self.currentColor = theme.chat.inputPanel.panelControlColor - let imageSize = CGSize(width: 44.0, height: 44.0) - let topRightPoint = CGPoint(x: 24.6, y: 14.0) - let centerPoint = CGPoint(x: 17.0, y: imageSize.height * 0.5) - self.arrowView.image = generateImage(imageSize, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(UIColor.white.cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setLineJoin(.round) - context.move(to: topRightPoint) - context.addLine(to: centerPoint) - context.addLine(to: CGPoint(x: topRightPoint.x, y: size.height - topRightPoint.y)) - context.strokePath() - })?.withRenderingMode(.alwaysTemplate) - self.arrowView.tintColor = theme.chat.inputPanel.panelControlColor + if self.currentColor != theme.rootController.navigationBar.accentTextColor { + self.currentColor = theme.rootController.navigationBar.accentTextColor + self.arrowView.image = NavigationBarTheme.generateBackArrowImage(color: theme.rootController.navigationBar.accentTextColor) } - let size = CGSize(width: 44.0, height: availableSize.height) + let iconSpacing: CGFloat = 8.0 + let iconOffset: CGFloat = -7.0 + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 13.0, height: 22.0) - let arrowFrame = arrowSize.centered(in: CGRect(origin: CGPoint(), size: size)) + let arrowFrame = CGRect(origin: CGPoint(x: iconOffset - 1.0, y: floor((availableSize.height - arrowSize.height) / 2.0)), size: arrowSize) transition.setPosition(view: self.arrowView, position: arrowFrame.center) transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) - return size + let titleFrame = CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if titleTextUpdated { + self.titleView.frame = titleFrame + } else { + transition.setFrame(view: self.titleView, frame: titleFrame) + } + + return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) } } @@ -296,9 +305,9 @@ public final class ChatListHeaderComponent: Component { let openStatusSetup: (UIView) -> Void let toggleIsLocked: () -> Void - let leftButtonsContainer: UIView + let leftButtonOffsetContainer: UIView var leftButtonViews: [AnyHashable: ComponentView] = [:] - let rightButtonsContainer: UIView + let rightButtonOffsetContainer: UIView var rightButtonViews: [AnyHashable: ComponentView] = [:] var backButtonView: BackButtonView? @@ -315,9 +324,6 @@ public final class ChatListHeaderComponent: Component { private(set) var centerContentOffsetX: CGFloat = 0.0 private(set) var centerContentOrigin: CGFloat = 0.0 - - private(set) var leftButtonsWidth: CGFloat = 0.0 - private(set) var rightButtonsWidth: CGFloat = 0.0 init( backPressed: @escaping () -> Void, @@ -328,9 +334,8 @@ public final class ChatListHeaderComponent: Component { self.openStatusSetup = openStatusSetup self.toggleIsLocked = toggleIsLocked - self.leftButtonsContainer = UIView() - self.rightButtonsContainer = UIView() - + self.leftButtonOffsetContainer = UIView() + self.rightButtonOffsetContainer = UIView() self.titleOffsetContainer = UIView() self.titleScaleContainer = UIView() @@ -340,6 +345,8 @@ public final class ChatListHeaderComponent: Component { self.addSubview(self.titleOffsetContainer) self.titleOffsetContainer.addSubview(self.titleScaleContainer) + self.addSubview(self.leftButtonOffsetContainer) + self.addSubview(self.rightButtonOffsetContainer) self.titleScaleContainer.addSubview(self.titleTextView) } @@ -386,12 +393,12 @@ public final class ChatListHeaderComponent: Component { transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform) } - func updateNavigationTransitionAsPrevious(nextView: ContentView, width: CGFloat, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) - alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) - alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: fraction * 10.0) - alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) - alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: fraction * 10.0) + func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { + transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in + completion() + }) + transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) if let backButtonView = self.backButtonView { transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: backButtonView.bounds.size), completion: { _ in @@ -399,53 +406,80 @@ public final class ChatListHeaderComponent: Component { }) } - let totalOffset = -width * fraction - - transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + if let chatListTitleView = self.chatListTitleView, let nextBackButtonView = nextView.backButtonView { + let titleFrame = chatListTitleView.titleNode.view.convert(chatListTitleView.titleNode.bounds, to: self.titleOffsetContainer) + let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: nextView) + + let totalOffset = titleFrame.minX - backButtonTitleFrame.minX + + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + } } func updateNavigationTransitionAsNext(previousView: ContentView, storyPeerListView: StoryPeerListComponent.View?, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) - transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) - alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) - - alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) - alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) + transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) + if let backButtonView = self.backButtonView { + transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) + transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) + + if let storyPeerListView { + let previousTitleFrame = storyPeerListView.titleFrame() + let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self) + + let totalOffset = previousTitleFrame.minX - backButtonTitleFrame.minX + + transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) + } else if let previousChatListTitleView = previousView.chatListTitleView { + let previousTitleFrame = previousChatListTitleView.titleNode.view.convert(previousChatListTitleView.titleNode.bounds, to: previousView.titleOffsetContainer) + let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self) + + let totalOffset = previousTitleFrame.minX - backButtonTitleFrame.minX + + transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) + } + } } func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) - - alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) - alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: fraction * 10.0) - alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in + transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in + }) + transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in completion() }) - alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: fraction * 10.0) + + if let backButtonView = self.backButtonView { + transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.bounds.size), completion: { _ in + }) + } transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size)) transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) } func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) - transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) - alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) - alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(fraction, 2.0)) - alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) - alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) + transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) + if let backButtonView = self.backButtonView { + transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) + transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) + + transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) + } } - func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, displayBackButton: Bool, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) - + func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size)) @@ -456,10 +490,11 @@ public final class ChatListHeaderComponent: Component { let titleTextUpdated = self.titleTextView.attributedText != titleText self.titleTextView.attributedText = titleText - let buttonSpacing: CGFloat = 0.0 - var nextLeftButtonX: CGFloat = 0.0 + let buttonSpacing: CGFloat = 8.0 - if displayBackButton { + var leftOffset = sideInset + + if let backTitle = backTitle { var backButtonTransition = transition let backButtonView: BackButtonView if let current = self.backButtonView { @@ -473,14 +508,11 @@ public final class ChatListHeaderComponent: Component { self.backPressed() }) self.backButtonView = backButtonView - self.leftButtonsContainer.addSubview(backButtonView) + self.addSubview(backButtonView) } - let backButtonSize = backButtonView.update(theme: theme, strings: strings, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition) - backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: nextLeftButtonX, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize)) - if nextLeftButtonX != 0.0 { - nextLeftButtonX += buttonSpacing - } - nextLeftButtonX += backButtonSize.width + let backButtonSize = backButtonView.update(title: backTitle, theme: theme, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition) + backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize)) + leftOffset += backButtonSize.width + buttonSpacing } else if let backButtonView = self.backButtonView { self.backButtonView = nil backButtonView.removeFromSuperview() @@ -489,10 +521,6 @@ public final class ChatListHeaderComponent: Component { var validLeftButtons = Set() if let leftButton = content.leftButton { validLeftButtons.insert(leftButton.id) - - if nextLeftButtonX != 0.0 { - nextLeftButtonX += buttonSpacing - } var buttonTransition = transition var animateButtonIn = false @@ -513,25 +541,23 @@ public final class ChatListHeaderComponent: Component { }, containerSize: CGSize(width: 100.0, height: size.height) ) - let buttonFrame = CGRect(origin: CGPoint(x: nextLeftButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { - self.leftButtonsContainer.addSubview(buttonComponentView) + self.leftButtonOffsetContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { - alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) - alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } - nextLeftButtonX += buttonSize.width + leftOffset = buttonFrame.maxX + buttonSpacing } var removeLeftButtons: [AnyHashable] = [] for (id, buttonView) in self.leftButtonViews { if !validLeftButtons.contains(id) { if let buttonComponentView = buttonView.view { - alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) - alphaTransition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in buttonComponentView?.removeFromSuperview() }) } @@ -542,14 +568,10 @@ public final class ChatListHeaderComponent: Component { self.leftButtonViews.removeValue(forKey: id) } - var nextRightButtonX: CGFloat = 0.0 + var rightOffset = size.width - sideInset var validRightButtons = Set() - for rightButton in content.rightButtons.reversed() { + for rightButton in content.rightButtons { validRightButtons.insert(rightButton.id) - - if nextRightButtonX != 0.0 { - nextRightButtonX += buttonSpacing - } var buttonTransition = transition var animateButtonIn = false @@ -570,25 +592,23 @@ public final class ChatListHeaderComponent: Component { }, containerSize: CGSize(width: 100.0, height: size.height) ) - let buttonFrame = CGRect(origin: CGPoint(x: nextRightButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: rightOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { - self.rightButtonsContainer.addSubview(buttonComponentView) + self.rightButtonOffsetContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { - alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) - alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } - nextRightButtonX += buttonSize.width + rightOffset = buttonFrame.minX - buttonSpacing } var removeRightButtons: [AnyHashable] = [] for (id, buttonView) in self.rightButtonViews { if !validRightButtons.contains(id) { if let buttonComponentView = buttonView.view { - alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) - alphaTransition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in buttonComponentView?.removeFromSuperview() }) } @@ -598,11 +618,8 @@ public final class ChatListHeaderComponent: Component { for id in removeRightButtons { self.rightButtonViews.removeValue(forKey: id) } - - self.leftButtonsWidth = nextLeftButtonX - self.rightButtonsWidth = nextRightButtonX - - let commonInset: CGFloat = sideInset + max(nextLeftButtonX, nextRightButtonX) + 8.0 + + let commonInset: CGFloat = max(leftOffset, size.width - rightOffset) let remainingWidth = size.width - commonInset * 2.0 let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) @@ -645,10 +662,10 @@ public final class ChatListHeaderComponent: Component { } var centerContentLeftInset: CGFloat = 0.0 - centerContentLeftInset = nextLeftButtonX + 4.0 + centerContentLeftInset = leftOffset - 4.0 var centerContentRightInset: CGFloat = 0.0 - centerContentRightInset = nextRightButtonX + 20.0 + centerContentRightInset = size.width - rightOffset - 8.0 var centerContentWidth: CGFloat = 0.0 var centerContentOffsetX: CGFloat = 0.0 @@ -673,8 +690,10 @@ public final class ChatListHeaderComponent: Component { let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition) centerContentWidth = floor((chatListTitleContentSize.width * 0.5 - titleContentRect.minX) * 2.0) + //sideWidth + centerWidth + centerOffset = size.width + //let centerOffset = -(size.width - (sideContentWidth + centerContentWidth)) * 0.5 + size.width * 0.5 let centerOffset = sideContentWidth * 0.5 - centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - (size.width - sideInset - nextRightButtonX)) + centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - rightOffset) chatListTitleView.openStatusSetup = { [weak self] sourceView in guard let self else { @@ -722,11 +741,6 @@ public final class ChatListHeaderComponent: Component { private var primaryContentView: ContentView? private var secondaryContentView: ContentView? private var storyOffsetFraction: CGFloat = 0.0 - - private let leftButtonsContainer: UIView - private let rightButtonsContainer: UIView - private var leftButtonsBackgroundContainer: GlassBackgroundView? - private var rightButtonsBackgroundContainer: GlassBackgroundView? private let storyPeerListExternalState = StoryPeerListComponent.ExternalState() private var storyPeerList: ComponentView? @@ -739,10 +753,6 @@ public final class ChatListHeaderComponent: Component { } override init(frame: CGRect) { - self.leftButtonsContainer = UIView() - self.rightButtonsContainer = UIView() - self.rightButtonsContainer.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) - super.init(frame: frame) self.storyOffsetFraction = 1.0 @@ -756,6 +766,10 @@ public final class ChatListHeaderComponent: Component { return self.effectiveContentView?.backButtonView?.arrowView } + public var backButtonTitleView: UIView? { + return self.effectiveContentView?.backButtonView?.titleView + } + public var rightButtonView: UIView? { return self.effectiveContentView?.rightButtonViews.first?.value.view } @@ -781,6 +795,18 @@ public final class ChatListHeaderComponent: Component { } } + public func makeTransitionBackButtonView(accentColor: UIColor) -> UIView? { + if let backButtonTitleView = self.backButtonTitleView as? ImmediateTextView { + let view = ImmediateTextView() + view.attributedText = NSAttributedString(string: backButtonTitleView.attributedText?.string ?? "", font: Font.regular(17.0), textColor: accentColor) + let _ = view.updateLayout(CGSize(width: 100.0, height: 100.0)) + view.frame = backButtonTitleView.convert(backButtonTitleView.bounds, to: self) + return view + } else { + return nil + } + } + public func storyPeerListView() -> StoryPeerListComponent.View? { return self.storyPeerList?.view as? StoryPeerListComponent.View } @@ -822,8 +848,8 @@ public final class ChatListHeaderComponent: Component { let previousComponent = self.component self.component = component - var primaryContentTransition = transition if var primaryContent = component.primaryContent { + var primaryContentTransition = transition let primaryContentView: ContentView if let current = self.primaryContentView { primaryContentView = current @@ -851,8 +877,6 @@ public final class ChatListHeaderComponent: Component { ) self.primaryContentView = primaryContentView self.addSubview(primaryContentView) - self.leftButtonsContainer.addSubview(primaryContentView.leftButtonsContainer) - self.rightButtonsContainer.addSubview(primaryContentView.rightButtonsContainer) } let sideContentWidth: CGFloat = 0.0 @@ -865,19 +889,18 @@ public final class ChatListHeaderComponent: Component { chatListTitle: nil, leftButton: primaryContent.leftButton, rightButtons: primaryContent.rightButtons, + backTitle: primaryContent.backTitle, backPressed: primaryContent.backPressed ) } - primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, displayBackButton: primaryContent.backPressed != nil, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) + primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition) } else if let primaryContentView = self.primaryContentView { self.primaryContentView = nil primaryContentView.removeFromSuperview() - primaryContentView.leftButtonsContainer.removeFromSuperview() - primaryContentView.rightButtonsContainer.removeFromSuperview() } var storyListTransition = transition @@ -968,16 +991,13 @@ public final class ChatListHeaderComponent: Component { ) } - var secondaryContentTransition = transition - var secondaryContentIsAnimatingIn = false - var removedSecondaryContentView: ContentView? if let secondaryContent = component.secondaryContent { + var secondaryContentTransition = transition let secondaryContentView: ContentView if let current = self.secondaryContentView { secondaryContentView = current } else { secondaryContentTransition = .immediate - secondaryContentIsAnimatingIn = true secondaryContentView = ContentView( backPressed: { [weak self] in guard let self, let component = self.component else { @@ -999,15 +1019,9 @@ public final class ChatListHeaderComponent: Component { } ) self.secondaryContentView = secondaryContentView - if let primaryContentView = self.primaryContentView { - self.insertSubview(secondaryContentView, aboveSubview: primaryContentView) - } else { - self.addSubview(secondaryContentView) - } - self.leftButtonsContainer.addSubview(secondaryContentView.leftButtonsContainer) - self.rightButtonsContainer.addSubview(secondaryContentView.rightButtonsContainer) + self.addSubview(secondaryContentView) } - secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, displayBackButton: true, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition) + secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition) secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition) @@ -1018,7 +1032,7 @@ public final class ChatListHeaderComponent: Component { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: 0.0, transition: .immediate, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: 0.0, transition: .immediate, completion: {}) } } @@ -1027,33 +1041,26 @@ public final class ChatListHeaderComponent: Component { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: component.secondaryTransition, transition: transition, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: component.secondaryTransition, transition: transition, completion: {}) } } } else if let secondaryContentView = self.secondaryContentView { self.secondaryContentView = nil - removedSecondaryContentView = secondaryContentView if let primaryContentView = self.primaryContentView { if self.storyOffsetFraction < 0.8 { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in - secondaryContentView?.leftButtonsContainer.removeFromSuperview() - secondaryContentView?.rightButtonsContainer.removeFromSuperview() secondaryContentView?.removeFromSuperview() }) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: 0.0, transition: transition, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in - secondaryContentView?.leftButtonsContainer.removeFromSuperview() - secondaryContentView?.rightButtonsContainer.removeFromSuperview() secondaryContentView?.removeFromSuperview() }) } } else { - secondaryContentView.leftButtonsContainer.removeFromSuperview() - secondaryContentView.rightButtonsContainer.removeFromSuperview() secondaryContentView.removeFromSuperview() } } @@ -1063,10 +1070,18 @@ public final class ChatListHeaderComponent: Component { self.addSubview(storyPeerListComponentView) } + //let storyPeerListMinOffset: CGFloat = -7.0 let storyPeerListMaxOffset: CGFloat = availableSize.height + 2.0 + //let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - component.storiesFraction) + storyPeerListMaxOffset * component.storiesFraction + var storiesX: CGFloat = 0.0 - storiesX -= availableSize.width * component.secondaryTransition + if let nextBackButtonView = self.secondaryContentView?.backButtonView { + let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: self) + let storyListTitleFrame = storyPeerListComponentView.titleFrame() + + storiesX += (backButtonTitleFrame.minX - storyListTitleFrame.minX) * component.secondaryTransition + } storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: storiesX, y: storyPeerListMaxOffset), size: CGSize(width: availableSize.width, height: 79.0))) @@ -1075,84 +1090,6 @@ public final class ChatListHeaderComponent: Component { let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha) } - - var leftButtonsEffectiveWidth: CGFloat = 0.0 - var rightButtonsEffectiveWidth: CGFloat = 0.0 - if let primaryContentView = self.primaryContentView, let secondaryContentView = self.secondaryContentView { - - leftButtonsEffectiveWidth = primaryContentView.leftButtonsWidth * (1.0 - component.secondaryTransition) + secondaryContentView.leftButtonsWidth * component.secondaryTransition - rightButtonsEffectiveWidth = primaryContentView.rightButtonsWidth * (1.0 - component.secondaryTransition) + secondaryContentView.rightButtonsWidth * component.secondaryTransition - - primaryContentTransition.setFrame(view: primaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.leftButtonsWidth), height: 44.0))) - secondaryContentTransition.setFrame(view: secondaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.leftButtonsWidth), height: 44.0))) - - primaryContentTransition.setFrame(view: primaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - primaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.rightButtonsWidth), height: 44.0))) - - if secondaryContentIsAnimatingIn { - secondaryContentView.rightButtonsContainer.frame = CGRect(origin: CGPoint(x: self.rightButtonsContainer.bounds.width - secondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.rightButtonsWidth), height: 44.0)) - } - transition.setFrame(view: secondaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - secondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.rightButtonsWidth), height: 44.0))) - } else if let primaryContentView = self.primaryContentView { - leftButtonsEffectiveWidth = primaryContentView.leftButtonsWidth - rightButtonsEffectiveWidth = primaryContentView.rightButtonsWidth - - primaryContentTransition.setFrame(view: primaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.leftButtonsWidth), height: 44.0))) - primaryContentTransition.setFrame(view: primaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - primaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.rightButtonsWidth), height: 44.0))) - - if let removedSecondaryContentView { - transition.setFrame(view: removedSecondaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - removedSecondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, removedSecondaryContentView.rightButtonsWidth), height: 44.0))) - } - } - - if leftButtonsEffectiveWidth != 0.0 { - let leftButtonsBackgroundContainer: GlassBackgroundView - var leftButtonsBackgroundContainerTransition = transition - if let current = self.leftButtonsBackgroundContainer { - leftButtonsBackgroundContainer = current - } else { - leftButtonsBackgroundContainerTransition = leftButtonsBackgroundContainerTransition.withAnimation(.none) - leftButtonsBackgroundContainer = GlassBackgroundView() - self.leftButtonsBackgroundContainer = leftButtonsBackgroundContainer - self.addSubview(leftButtonsBackgroundContainer) - leftButtonsBackgroundContainer.contentView.addSubview(self.leftButtonsContainer) - } - let leftButtonsContainerFrame = CGRect(origin: CGPoint(x: component.sideInset, y: 0.0), size: CGSize(width: max(44.0, leftButtonsEffectiveWidth), height: 44.0)) - leftButtonsBackgroundContainerTransition.setFrame(view: leftButtonsBackgroundContainer, frame: leftButtonsContainerFrame) - leftButtonsBackgroundContainer.update(size: leftButtonsContainerFrame.size, cornerRadius: leftButtonsContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: leftButtonsBackgroundContainerTransition) - leftButtonsBackgroundContainerTransition.setFrame(view: self.leftButtonsContainer, frame: CGRect(origin: CGPoint(), size: leftButtonsContainerFrame.size)) - } else { - if let leftButtonsBackgroundContainer = self.leftButtonsBackgroundContainer { - self.leftButtonsBackgroundContainer = nil - transition.setAlpha(view: leftButtonsBackgroundContainer, alpha: 0.0, completion: { [weak leftButtonsBackgroundContainer] _ in - leftButtonsBackgroundContainer?.removeFromSuperview() - }) - } - } - - if rightButtonsEffectiveWidth != 0.0 { - let rightButtonsBackgroundContainer: GlassBackgroundView - var rightButtonsBackgroundContainerTransition = transition - if let current = self.rightButtonsBackgroundContainer { - rightButtonsBackgroundContainer = current - } else { - rightButtonsBackgroundContainerTransition = rightButtonsBackgroundContainerTransition.withAnimation(.none) - rightButtonsBackgroundContainer = GlassBackgroundView() - self.rightButtonsBackgroundContainer = rightButtonsBackgroundContainer - self.addSubview(rightButtonsBackgroundContainer) - rightButtonsBackgroundContainer.contentView.addSubview(self.rightButtonsContainer) - } - let rightButtonsContainerFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - max(44.0, rightButtonsEffectiveWidth), y: 0.0), size: CGSize(width: max(44.0, rightButtonsEffectiveWidth), height: 44.0)) - rightButtonsBackgroundContainerTransition.setFrame(view: rightButtonsBackgroundContainer, frame: rightButtonsContainerFrame) - rightButtonsBackgroundContainer.update(size: rightButtonsContainerFrame.size, cornerRadius: rightButtonsContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: rightButtonsBackgroundContainerTransition) - rightButtonsBackgroundContainerTransition.setFrame(view: self.rightButtonsContainer, frame: CGRect(origin: CGPoint(), size: rightButtonsContainerFrame.size)) - } else { - if let rightButtonsBackgroundContainer = self.rightButtonsBackgroundContainer { - self.rightButtonsBackgroundContainer = nil - transition.setAlpha(view: rightButtonsBackgroundContainer, alpha: 0.0, completion: { [weak rightButtonsBackgroundContainer] _ in - rightButtonsBackgroundContainer?.removeFromSuperview() - }) - } - } return availableSize } @@ -1182,3 +1119,251 @@ public final class ChatListHeaderComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class NavigationButtonComponentEnvironment: Equatable { + public let theme: PresentationTheme + + public init(theme: PresentationTheme) { + self.theme = theme + } + + public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { + if lhs.theme != rhs.theme { + return false + } + return true + } +} + +public final class NavigationButtonComponent: Component { + public typealias EnvironmentType = NavigationButtonComponentEnvironment + + public enum Content: Equatable { + case text(title: String, isBold: Bool) + case more + case icon(imageName: String) + case proxy(status: ChatTitleProxyStatus) + } + + public let content: Content + public let pressed: (UIView) -> Void + public let contextAction: ((UIView, ContextGesture?) -> Void)? + + public init( + content: Content, + pressed: @escaping (UIView) -> Void, + contextAction: ((UIView, ContextGesture?) -> Void)? = nil + ) { + self.content = content + self.pressed = pressed + self.contextAction = contextAction + } + + public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var textView: ImmediateTextView? + + private var iconView: UIImageView? + private var iconImageName: String? + + private var proxyNode: ChatTitleProxyNode? + + private var moreButton: MoreHeaderButton? + + private var component: NavigationButtonComponent? + private var theme: PresentationTheme? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.textView?.alpha = 0.6 + self.proxyNode?.alpha = 0.6 + self.iconView?.alpha = 0.6 + } else { + self.textView?.alpha = 1.0 + self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.proxyNode?.alpha = 1.0 + self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.iconView?.alpha = 1.0 + self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.pressed(self) + } + + func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let theme = environment[NavigationButtonComponentEnvironment.self].value.theme + var themeUpdated = false + if self.theme !== theme { + self.theme = theme + themeUpdated = true + } + + let iconOffset: CGFloat = 4.0 + + var textString: NSAttributedString? + var imageName: String? + var proxyStatus: ChatTitleProxyStatus? + var isMore: Bool = false + + switch component.content { + case let .text(title, isBold): + textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + case .more: + isMore = true + case let .icon(imageNameValue): + imageName = imageNameValue + case let .proxy(status): + proxyStatus = status + } + + var size = CGSize(width: 0.0, height: availableSize.height) + + if let textString = textString { + let textView: ImmediateTextView + if let current = self.textView { + textView = current + } else { + textView = ImmediateTextView() + textView.isUserInteractionEnabled = false + self.textView = textView + self.addSubview(textView) + } + + textView.attributedText = textString + let textSize = textView.updateLayout(availableSize) + size.width = textSize.width + + textView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) + } else if let textView = self.textView { + self.textView = nil + textView.removeFromSuperview() + } + + if let imageName = imageName { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + iconView = UIImageView() + iconView.isUserInteractionEnabled = false + self.iconView = iconView + self.addSubview(iconView) + } + if self.iconImageName != imageName || themeUpdated { + self.iconImageName = imageName + iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.rootController.navigationBar.accentTextColor) + } + + if let iconSize = iconView.image?.size { + size.width = iconSize.width + + iconView.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) + } + } else if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + self.iconImageName = nil + } + + if let proxyStatus = proxyStatus { + let proxyNode: ChatTitleProxyNode + if let current = self.proxyNode { + proxyNode = current + } else { + proxyNode = ChatTitleProxyNode(theme: theme) + proxyNode.isUserInteractionEnabled = false + self.proxyNode = proxyNode + self.addSubnode(proxyNode) + } + + let proxySize = CGSize(width: 30.0, height: 30.0) + size.width = proxySize.width + + proxyNode.theme = theme + proxyNode.status = proxyStatus + + proxyNode.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) + } else if let proxyNode = self.proxyNode { + self.proxyNode = nil + proxyNode.removeFromSupernode() + } + + if isMore { + let moreButton: MoreHeaderButton + if let current = self.moreButton, !themeUpdated { + moreButton = current + } else { + if let moreButton = self.moreButton { + moreButton.removeFromSupernode() + self.moreButton = nil + } + + moreButton = MoreHeaderButton(color: theme.rootController.navigationBar.buttonColor) + moreButton.isUserInteractionEnabled = true + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) + moreButton.onPressed = { [weak self] in + guard let self, let component = self.component else { + return + } + self.moreButton?.play() + component.pressed(self) + } + moreButton.contextAction = { [weak self] sourceNode, gesture in + guard let self, let component = self.component else { + return + } + self.moreButton?.play() + component.contextAction?(self, gesture) + } + self.moreButton = moreButton + self.addSubnode(moreButton) + } + + let buttonSize = CGSize(width: 26.0, height: 44.0) + size.width = buttonSize.width + + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) + + moreButton.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - buttonSize.height) / 2.0)), size: buttonSize) + } else if let moreButton = self.moreButton { + self.moreButton = nil + moreButton.removeFromSupernode() + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index dc26cb74e3..549c6fb4fd 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -9,7 +9,6 @@ import SearchUI import AccountContext import TelegramCore import StoryPeerListComponent -import EdgeEffect public final class ChatListNavigationBar: Component { public final class AnimationHint { @@ -21,22 +20,14 @@ public final class ChatListNavigationBar: Component { self.crossfadeStoryPeers = crossfadeStoryPeers } } - - public struct Search: Equatable { - public var isEnabled: Bool - - public init(isEnabled: Bool) { - self.isEnabled = isEnabled - } - } public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings public let statusBarHeight: CGFloat public let sideInset: CGFloat - public let search: Search? public let isSearchActive: Bool + public let isSearchEnabled: Bool public let primaryContent: ChatListHeaderComponent.Content? public let secondaryContent: ChatListHeaderComponent.Content? public let secondaryTransition: CGFloat @@ -57,8 +48,8 @@ public final class ChatListNavigationBar: Component { strings: PresentationStrings, statusBarHeight: CGFloat, sideInset: CGFloat, - search: Search?, isSearchActive: Bool, + isSearchEnabled: Bool, primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?, secondaryTransition: CGFloat, @@ -78,8 +69,8 @@ public final class ChatListNavigationBar: Component { self.strings = strings self.statusBarHeight = statusBarHeight self.sideInset = sideInset - self.search = search self.isSearchActive = isSearchActive + self.isSearchEnabled = isSearchEnabled self.primaryContent = primaryContent self.secondaryContent = secondaryContent self.secondaryTransition = secondaryTransition @@ -111,10 +102,10 @@ public final class ChatListNavigationBar: Component { if lhs.sideInset != rhs.sideInset { return false } - if lhs.search != rhs.search { + if lhs.isSearchActive != rhs.isSearchActive { return false } - if lhs.isSearchActive != rhs.isSearchActive { + if lhs.isSearchEnabled != rhs.isSearchEnabled { return false } if lhs.primaryContent != rhs.primaryContent { @@ -164,7 +155,8 @@ public final class ChatListNavigationBar: Component { }() public final class View: UIView { - private let edgeEffectView: EdgeEffectView + private let backgroundView: BlurredBackgroundView + private let separatorLayer: SimpleLayer public let headerContent = ComponentView() @@ -192,15 +184,17 @@ public final class ChatListNavigationBar: Component { private var disappearingTabsViewSearch: Bool = false private var currentHeaderComponent: ChatListHeaderComponent? - - private var currentHeight: CGFloat = 0.0 override public init(frame: CGRect) { - self.edgeEffectView = EdgeEffectView() + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0) + self.separatorLayer = SimpleLayer() + self.separatorLayer.anchorPoint = CGPoint() super.init(frame: frame) - self.addSubview(self.edgeEffectView) + self.addSubview(self.backgroundView) + self.layer.addSublayer(self.separatorLayer) } required init?(coder: NSCoder) { @@ -208,7 +202,7 @@ public final class ChatListNavigationBar: Component { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if point.y >= self.currentHeight { + if !self.backgroundView.frame.contains(point) { return nil } @@ -254,12 +248,7 @@ public final class ChatListNavigationBar: Component { let searchOffsetDistance: CGFloat = ChatListNavigationBar.searchScrollHeight - let minContentOffset: CGFloat - if component.search != nil { - minContentOffset = ChatListNavigationBar.searchScrollHeight - } else { - minContentOffset = 0.0 - } + let minContentOffset: CGFloat = ChatListNavigationBar.searchScrollHeight let clippedScrollOffset = min(minContentOffset, offset) if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset && !forceUpdate && !allowAvatarsExpansionUpdated { @@ -270,87 +259,72 @@ public final class ChatListNavigationBar: Component { let visibleSize = CGSize(width: currentLayout.size.width, height: max(0.0, currentLayout.size.height - clippedScrollOffset)) - let previousHeight = self.currentHeight + let previousHeight = self.separatorLayer.position.y - /*self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition) + self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition) transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: visibleSize.width, height: 1000.0))) transition.animatePosition(view: self.backgroundView, from: CGPoint(x: 0.0, y: -visibleSize.height + self.backgroundView.layer.position.y), to: CGPoint(), additive: true) - self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height)*/ - - let edgeEffectHeight: CGFloat = currentLayout.size.height + 20.0 - var edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: currentLayout.size.width, height: edgeEffectHeight)) - if component.isSearchActive { - edgeEffectFrame.origin.y -= 46.0 - } - transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) - self.edgeEffectView.update(content: component.theme.list.plainBackgroundColor, blur: true, alpha: 0.75, rect: edgeEffectFrame, edge: .top, edgeSize: 40.0, transition: transition) + self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height) - self.currentHeight = visibleSize.height + transition.setFrameWithAdditivePosition(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel))) - if let search = component.search { - let searchContentNode: NavigationBarSearchContentNode - if let current = self.searchContentNode { - searchContentNode = current - - if themeUpdated { - let placeholder: String - let compactPlaceholder: String - - placeholder = component.strings.Common_Search - compactPlaceholder = component.strings.Common_Search - - searchContentNode.updateThemeAndPlaceholder(theme: component.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder) - } - } else { + let searchContentNode: NavigationBarSearchContentNode + if let current = self.searchContentNode { + searchContentNode = current + + if themeUpdated { let placeholder: String let compactPlaceholder: String placeholder = component.strings.Common_Search compactPlaceholder = component.strings.Common_Search - searchContentNode = NavigationBarSearchContentNode( - theme: component.theme, - placeholder: placeholder, - compactPlaceholder: compactPlaceholder, - activate: { [weak self] in - guard let self, let component = self.component, let searchContentNode = self.searchContentNode else { - return - } - component.activateSearch(searchContentNode) - } - ) - searchContentNode.view.layer.anchorPoint = CGPoint() - self.searchContentNode = searchContentNode - self.addSubview(searchContentNode.view) + searchContentNode.updateThemeAndPlaceholder(theme: component.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder) } - - let searchSize = CGSize(width: currentLayout.size.width - 6.0 * 2.0, height: navigationBarSearchContentHeight) - var searchFrame = CGRect(origin: CGPoint(x: 6.0, y: visibleSize.height - searchSize.height), size: searchSize) - if component.tabsNode != nil { - searchFrame.origin.y -= 40.0 - } - if !component.isSearchActive { - searchFrame.origin.y -= component.accessoryPanelContainerHeight - } - - let clippedSearchOffset = max(0.0, min(clippedScrollOffset, searchOffsetDistance)) - let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance - searchContentNode.expansionProgress = 1.0 - searchOffsetFraction - - transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame) - - searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) - - transition.setAlpha(view: searchContentNode.view, alpha: search.isEnabled ? 1.0 : 0.5) - searchContentNode.isUserInteractionEnabled = search.isEnabled } else { - if let searchContentNode = self.searchContentNode { - self.searchContentNode = nil - searchContentNode.view.removeFromSuperview() - } + let placeholder: String + let compactPlaceholder: String + + placeholder = component.strings.Common_Search + compactPlaceholder = component.strings.Common_Search + + searchContentNode = NavigationBarSearchContentNode( + theme: component.theme, + placeholder: placeholder, + compactPlaceholder: compactPlaceholder, + activate: { [weak self] in + guard let self, let component = self.component, let searchContentNode = self.searchContentNode else { + return + } + component.activateSearch(searchContentNode) + } + ) + searchContentNode.view.layer.anchorPoint = CGPoint() + self.searchContentNode = searchContentNode + self.addSubview(searchContentNode.view) } + let searchSize = CGSize(width: currentLayout.size.width - 6.0 * 2.0, height: navigationBarSearchContentHeight) + var searchFrame = CGRect(origin: CGPoint(x: 6.0, y: visibleSize.height - searchSize.height), size: searchSize) + if component.tabsNode != nil { + searchFrame.origin.y -= 40.0 + } + if !component.isSearchActive { + searchFrame.origin.y -= component.accessoryPanelContainerHeight + } + + let clippedSearchOffset = max(0.0, min(clippedScrollOffset, searchOffsetDistance)) + let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance + searchContentNode.expansionProgress = 1.0 - searchOffsetFraction + + transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame) + + searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) + + transition.setAlpha(view: searchContentNode.view, alpha: component.isSearchEnabled ? 1.0 : 0.5) + searchContentNode.isUserInteractionEnabled = component.isSearchEnabled + let headerTransition = transition let storiesOffsetFraction: CGFloat @@ -481,8 +455,7 @@ public final class ChatListNavigationBar: Component { } var tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: 46.0)) - if component.isSearchActive { - } else { + if !component.isSearchActive { tabsFrame.origin.y -= component.accessoryPanelContainerHeight } if component.tabsNode != nil { @@ -490,8 +463,7 @@ public final class ChatListNavigationBar: Component { } var accessoryPanelContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: component.accessoryPanelContainerHeight)) - if component.isSearchActive { - } else { + if !component.isSearchActive { accessoryPanelContainerFrame.origin.y -= component.accessoryPanelContainerHeight } @@ -556,8 +528,8 @@ public final class ChatListNavigationBar: Component { strings: component.strings, statusBarHeight: component.statusBarHeight, sideInset: component.sideInset, - search: component.search, isSearchActive: component.isSearchActive, + isSearchEnabled: component.isSearchEnabled, primaryContent: component.primaryContent, secondaryContent: component.secondaryContent, secondaryTransition: component.secondaryTransition, @@ -603,6 +575,8 @@ public final class ChatListNavigationBar: Component { } func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let themeUpdated = self.component?.theme !== component.theme + var uploadProgressUpdated = false var storySubscriptionsUpdated = false if let previousComponent = self.component { @@ -617,20 +591,23 @@ public final class ChatListNavigationBar: Component { self.component = component self.state = state + if themeUpdated { + self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) + self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor + } + var contentHeight = component.statusBarHeight if component.statusBarHeight >= 1.0 { contentHeight += 3.0 } contentHeight += 44.0 - contentHeight += 8.0 if component.isSearchActive { if component.statusBarHeight < 1.0 { contentHeight += 8.0 } - } - if component.search != nil { + } else { contentHeight += navigationBarSearchContentHeight } @@ -638,11 +615,8 @@ public final class ChatListNavigationBar: Component { contentHeight += 40.0 } - if component.isSearchActive { - } else { - if component.accessoryPanelContainer != nil { - contentHeight += component.accessoryPanelContainerHeight - } + if component.accessoryPanelContainer != nil && !component.isSearchActive { + contentHeight += component.accessoryPanelContainerHeight } let size = CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift deleted file mode 100644 index 4027524c18..0000000000 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift +++ /dev/null @@ -1,254 +0,0 @@ -import Foundation -import UIKit -import ComponentFlow -import ChatListTitleView -import TelegramPresentationData -import Display -import MoreHeaderButton - -public final class NavigationButtonComponentEnvironment: Equatable { - public let theme: PresentationTheme - - public init(theme: PresentationTheme) { - self.theme = theme - } - - public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { - if lhs.theme != rhs.theme { - return false - } - return true - } -} - -public final class NavigationButtonComponent: Component { - public typealias EnvironmentType = NavigationButtonComponentEnvironment - - public enum Content: Equatable { - case text(title: String, isBold: Bool) - case more - case icon(imageName: String) - case proxy(status: ChatTitleProxyStatus) - } - - public let content: Content - public let pressed: (UIView) -> Void - public let contextAction: ((UIView, ContextGesture?) -> Void)? - - public init( - content: Content, - pressed: @escaping (UIView) -> Void, - contextAction: ((UIView, ContextGesture?) -> Void)? = nil - ) { - self.content = content - self.pressed = pressed - self.contextAction = contextAction - } - - public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { - if lhs.content != rhs.content { - return false - } - return true - } - - public final class View: HighlightTrackingButton { - private var textView: ImmediateTextView? - - private var iconView: UIImageView? - private var iconImageName: String? - - private var proxyNode: ChatTitleProxyNode? - - private var moreButton: MoreHeaderButton? - - private var component: NavigationButtonComponent? - private var theme: PresentationTheme? - - override init(frame: CGRect) { - super.init(frame: frame) - - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - - self.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if highlighted { - self.textView?.alpha = 0.6 - self.proxyNode?.alpha = 0.6 - self.iconView?.alpha = 0.6 - } else { - self.textView?.alpha = 1.0 - self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - - self.proxyNode?.alpha = 1.0 - self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - - self.iconView?.alpha = 1.0 - self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - } - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func pressed() { - self.component?.pressed(self) - } - - func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - - let theme = environment[NavigationButtonComponentEnvironment.self].value.theme - var themeUpdated = false - if self.theme !== theme { - self.theme = theme - themeUpdated = true - } - - var textString: NSAttributedString? - var imageName: String? - var proxyStatus: ChatTitleProxyStatus? - var isMore: Bool = false - - switch component.content { - case let .text(title, isBold): - textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlColor) - case .more: - isMore = true - case let .icon(imageNameValue): - imageName = imageNameValue - case let .proxy(status): - proxyStatus = status - } - - var size = CGSize(width: 0.0, height: availableSize.height) - - if let textString = textString { - let textView: ImmediateTextView - if let current = self.textView { - textView = current - } else { - textView = ImmediateTextView() - textView.isUserInteractionEnabled = false - self.textView = textView - self.addSubview(textView) - } - - textView.attributedText = textString - let textSize = textView.updateLayout(availableSize) - let textInset: CGFloat = 10.0 - size.width = max(44.0, textSize.width + textInset * 2.0) - - textView.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) - } else if let textView = self.textView { - self.textView = nil - textView.removeFromSuperview() - } - - if let imageName = imageName { - let iconView: UIImageView - if let current = self.iconView { - iconView = current - } else { - iconView = UIImageView() - iconView.isUserInteractionEnabled = false - self.iconView = iconView - self.addSubview(iconView) - } - if self.iconImageName != imageName || themeUpdated { - self.iconImageName = imageName - iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.chat.inputPanel.panelControlColor) - } - - if let iconSize = iconView.image?.size { - size.width = 44.0 - - iconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) - } - } else if let iconView = self.iconView { - self.iconView = nil - iconView.removeFromSuperview() - self.iconImageName = nil - } - - if let proxyStatus = proxyStatus { - let proxyNode: ChatTitleProxyNode - if let current = self.proxyNode { - proxyNode = current - } else { - proxyNode = ChatTitleProxyNode(theme: theme) - proxyNode.isUserInteractionEnabled = false - self.proxyNode = proxyNode - self.addSubnode(proxyNode) - } - - let proxySize = CGSize(width: 30.0, height: 30.0) - size.width = 44.0 - - proxyNode.theme = theme - proxyNode.status = proxyStatus - - proxyNode.frame = CGRect(origin: CGPoint(x: floor((size.width - proxySize.width) / 2.0), y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) - } else if let proxyNode = self.proxyNode { - self.proxyNode = nil - proxyNode.removeFromSupernode() - } - - if isMore { - let moreButton: MoreHeaderButton - if let current = self.moreButton, !themeUpdated { - moreButton = current - } else { - if let moreButton = self.moreButton { - moreButton.removeFromSupernode() - self.moreButton = nil - } - - moreButton = MoreHeaderButton(color: theme.chat.inputPanel.panelControlColor) - moreButton.isUserInteractionEnabled = true - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.chat.inputPanel.panelControlColor))) - moreButton.onPressed = { [weak self] in - guard let self, let component = self.component else { - return - } - self.moreButton?.play() - component.pressed(self) - } - moreButton.contextAction = { [weak self] sourceNode, gesture in - guard let self, let component = self.component else { - return - } - self.moreButton?.play() - component.contextAction?(self, gesture) - } - self.moreButton = moreButton - self.addSubnode(moreButton) - } - - let buttonSize = CGSize(width: 30.0, height: 44.0) - size.width = 44.0 - - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) - - moreButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) - } else if let moreButton = self.moreButton { - self.moreButton = nil - moreButton.removeFromSupernode() - } - - return size - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift deleted file mode 100644 index bdc1e32e4e..0000000000 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import UIKit -import ComponentFlow - -final class NavigationButtonContainerView: UIView { -} diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift index b3d2606b47..5ec474b50d 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift @@ -44,13 +44,13 @@ public final class ChatTitleProxyNode: ASDisplayNode { if self.theme !== oldValue { switch self.status { case .connecting: - self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: false) + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) case .connected: - self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: true, off: false) + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) case .available: - self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) } - self.activityIndicator.type = .custom(theme.chat.inputPanel.panelControlColor, 10.0, 1.3333, true) + self.activityIndicator.type = .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.3333, true) } } } @@ -61,13 +61,13 @@ public final class ChatTitleProxyNode: ASDisplayNode { switch self.status { case .connecting: self.activityIndicator.isHidden = false - self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: false) + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) case .connected: self.activityIndicator.isHidden = true - self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: true, off: false) + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) case .available: self.activityIndicator.isHidden = true - self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) } } } @@ -80,9 +80,9 @@ public final class ChatTitleProxyNode: ASDisplayNode { self.iconNode.isLayerBacked = true self.iconNode.displayWithoutProcessing = true self.iconNode.displaysAsynchronously = false - self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) - self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlColor, 10.0, 1.3333, true), speed: .slow) + self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.3333, true), speed: .slow) super.init() diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index 040181f130..e5d1e84948 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -64,15 +64,13 @@ public final class LiquidLensView: UIView { var selectionWidth: CGFloat var isDark: Bool var isLifted: Bool - var isCollapsed: Bool - init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { + init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool) { self.size = size self.selectionX = selectionX self.selectionWidth = selectionWidth self.isLifted = isLifted self.isDark = isDark - self.isCollapsed = isCollapsed } } @@ -87,8 +85,8 @@ public final class LiquidLensView: UIView { } private let containerView: UIView - private let backgroundContainer: GlassBackgroundContainerView? - private let genericBackgroundContainer: UIView? + private let backgroundContainerContainer: UIView + private let backgroundContainer: GlassBackgroundContainerView private let backgroundView: GlassBackgroundView private var lensView: UIView? private let liftedContainerView: UIView @@ -119,16 +117,11 @@ public final class LiquidLensView: UIView { return self.params?.selectionWidth } - public init(useBackgroundContainer: Bool = true) { + override public init(frame: CGRect) { self.containerView = UIView() - if useBackgroundContainer { - self.backgroundContainer = GlassBackgroundContainerView() - self.genericBackgroundContainer = nil - } else { - self.backgroundContainer = nil - self.genericBackgroundContainer = UIView() - } + self.backgroundContainerContainer = UIView() + self.backgroundContainer = GlassBackgroundContainerView() self.backgroundView = GlassBackgroundView() @@ -137,16 +130,12 @@ public final class LiquidLensView: UIView { self.restingBackgroundView = RestingBackgroundView() - super.init(frame: CGRect()) + super.init(frame: frame) - if let backgroundContainer = self.backgroundContainer { - self.addSubview(backgroundContainer) - backgroundContainer.contentView.addSubview(self.backgroundView) - } else if let genericBackgroundContainer = self.genericBackgroundContainer { - self.addSubview(genericBackgroundContainer) - genericBackgroundContainer.addSubview(self.backgroundView) - } + self.backgroundContainerContainer.addSubview(self.backgroundContainer) + self.addSubview(self.backgroundContainerContainer) + self.backgroundContainer.contentView.addSubview(self.backgroundView) self.backgroundView.contentView.addSubview(self.containerView) self.containerView.isUserInteractionEnabled = false @@ -161,11 +150,7 @@ public final class LiquidLensView: UIView { } if let lensView = self.lensView { - if let backgroundContainer = self.backgroundContainer { - backgroundContainer.layer.zPosition = 1 - } else if let genericBackgroundContainer = self.genericBackgroundContainer{ - genericBackgroundContainer.layer.zPosition = 1 - } + self.backgroundContainer.layer.zPosition = 1 lensView.layer.zPosition = 10.0 self.liftedContainerView.addSubview(self.restingBackgroundView) @@ -174,11 +159,7 @@ public final class LiquidLensView: UIView { self.containerView.addSubview(lensView) self.containerView.addSubview(self.contentView) - if let backgroundContainer = self.backgroundContainer { - lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: backgroundContainer.contentView) - } else if let genericBackgroundContainer = self.genericBackgroundContainer { - lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: genericBackgroundContainer) - } + lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: self.backgroundContainer.contentView) lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView) lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView) @@ -242,8 +223,8 @@ public final class LiquidLensView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { - let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) + public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, transition: ComponentTransition) { + let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted) if self.params == params { return } @@ -338,36 +319,16 @@ public final class LiquidLensView: UIView { self.params = params transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.backgroundContainerContainer, frame: CGRect(origin: CGPoint(), size: params.size)) - if let backgroundContainer = self.backgroundContainer { - transition.setFrame(view: backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) - backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition) - } else if let genericBackgroundContainer = self.genericBackgroundContainer { - transition.setFrame(view: genericBackgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) - } + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) + self.backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) - if self.contentView.bounds.size != params.size { - self.contentView.clipsToBounds = true - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in - guard let self, completed else { - return - } - self.contentView.clipsToBounds = false - }) - transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: params.size.height * 0.5) - - self.liftedContainerView.clipsToBounds = true - transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in - guard let self, completed else { - return - } - self.liftedContainerView.clipsToBounds = false - }) - transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.size.height * 0.5) - } + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size)) + transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size)) let baseLensFrame = CGRect(origin: CGPoint(x: max(0.0, min(params.selectionX, params.size.width - params.selectionWidth)), y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height)) self.updateLens(params: LensParams(baseFrame: baseLensFrame, isLifted: params.isLifted), animated: !transition.animation.isImmediate) @@ -393,7 +354,7 @@ public final class LiquidLensView: UIView { transition.setFrame(view: self.restingBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.restingBackgroundView.update(isDark: params.isDark) - transition.setAlpha(view: self.restingBackgroundView, alpha: (params.isLifted || params.isCollapsed) ? 0.0 : 1.0) + transition.setAlpha(view: self.restingBackgroundView, alpha: params.isLifted ? 0.0 : 1.0) if params.isLifted { if self.liftedDisplayLink == nil { diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift index 5cb901cfde..579bd7b8d9 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -266,7 +266,8 @@ final class MiniAppListScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backPressed: isModal ? nil : { [weak self] in + backTitle: isModal ? nil : strings.Common_Back, + backPressed: { [weak self] in guard let self else { return } @@ -285,8 +286,8 @@ final class MiniAppListScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, - search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c05923ba09..ad318df6ff 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -13923,8 +13923,6 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } self._readyProxy.set(.single(true)) }) - - self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required init(coder aDecoder: NSCoder) { @@ -14478,7 +14476,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar { self.addSubnode(bottomNavigationBar.additionalContentNode) - /*if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { + if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { if let previousBackButtonArrow = headerView.makeTransitionBackArrowView(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { self.previousBackButtonArrow = previousBackButtonArrow self.view.addSubview(previousBackButtonArrow) @@ -14487,7 +14485,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig self.previousBackButton = previousBackButton self.view.addSubview(previousBackButton) } - } else*/ do { + } else { if let previousBackButtonArrow = bottomNavigationBar.makeTransitionBackArrowView(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { self.previousBackButtonArrow = previousBackButtonArrow self.view.addSubview(previousBackButtonArrow) @@ -14596,13 +14594,13 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } if let previousBackButton = self.previousBackButton { - /*if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { + if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { if let backButtonTitleView = headerView.backButtonTitleView { let previousBackButtonFrame = backButtonTitleView.convert(backButtonTitleView.bounds, to: bottomNavigationBar.view) previousBackButton.frame = previousBackButtonFrame transition.updateAlpha(layer: previousBackButton.layer, alpha: fraction) } - } else*/ do { + } else { let previousBackButtonFrame = bottomNavigationBar.backButtonNode.view.convert(bottomNavigationBar.backButtonNode.view.bounds, to: bottomNavigationBar.view) previousBackButton.frame = previousBackButtonFrame transition.updateAlpha(layer: previousBackButton.layer, alpha: fraction) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/BUILD b/submodules/TelegramUI/Components/PeerSelectionController/BUILD index f2c10542eb..230b96365d 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/BUILD +++ b/submodules/TelegramUI/Components/PeerSelectionController/BUILD @@ -35,7 +35,6 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode", "//submodules/CounterControllerTitleView", - "//submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index 0dc5f6b517..db095daadd 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -10,7 +10,6 @@ import AccountContext import SearchUI import ChatListUI import CounterControllerTitleView -import ChatListFilterTabContainerNode public final class PeerSelectionControllerImpl: ViewController, PeerSelectionController { private let context: AccountContext diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 0114593bb9..67c0665476 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -798,7 +798,8 @@ final class QuickReplySetupScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backPressed: isModal ? nil :{ [weak self] in + backTitle: isModal ? nil : strings.Common_Back, + backPressed: { [weak self] in guard let self else { return } @@ -817,8 +818,8 @@ final class QuickReplySetupScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, - search: ChatListNavigationBar.Search(isEnabled: !self.isEditing), isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: !self.isEditing, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index 8aadf7da54..b99ea5e371 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -298,7 +298,8 @@ final class PeerSelectionScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backPressed: isModal ? nil : { [weak self] in + backTitle: isModal ? nil : strings.Common_Back, + backPressed: { [weak self] in guard let self else { return } @@ -317,8 +318,8 @@ final class PeerSelectionScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, - search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, + isSearchEnabled: true, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/TabBarComponent/BUILD b/submodules/TelegramUI/Components/TabBarComponent/BUILD index e08cc9e22b..bf1656c7b3 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/BUILD +++ b/submodules/TelegramUI/Components/TabBarComponent/BUILD @@ -22,7 +22,6 @@ swift_library( "//submodules/UIKitRuntimeUtils", "//submodules/TelegramUI/Components/LiquidLens", "//submodules/AppBundle", - "//submodules/SearchBarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 010a270d59..585e15ed8c 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -12,7 +12,6 @@ import BundleIconComponent import TextBadgeComponent import LiquidLens import AppBundle -import SearchBarNode private final class TabSelectionRecognizer: UIGestureRecognizer { private var initialLocation: CGPoint? @@ -70,147 +69,35 @@ private final class TabSelectionRecognizer: UIGestureRecognizer { } } -public final class NavigationSearchView: UIView { - private struct Params: Equatable { - let size: CGSize - let theme: PresentationTheme - let strings: PresentationStrings - let isActive: Bool - - init(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool) { - self.size = size - self.theme = theme - self.strings = strings - self.isActive = isActive - } - - static func ==(lhs: Params, rhs: Params) -> Bool { - if lhs.size != rhs.size { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.isActive != rhs.isActive { - return false - } - return true - } - } - - private let action: () -> Void - +public final class TabBarSearchView: UIView { private let backgroundView: GlassBackgroundView - private let iconView: UIImageView - private(set) var searchBarNode: SearchBarNode? - - private var params: Params? + private let iconView: GlassBackgroundView.ContentImageView - public init(action: @escaping () -> Void) { - self.action = action - + override public init(frame: CGRect) { self.backgroundView = GlassBackgroundView() - self.backgroundView.contentView.clipsToBounds = true - self.iconView = UIImageView() + self.iconView = GlassBackgroundView.ContentImageView() - super.init(frame: CGRect()) + super.init(frame: frame) self.addSubview(self.backgroundView) self.backgroundView.contentView.addSubview(self.iconView) - - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) - } - - @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.action() - } } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool, transition: ComponentTransition) { - let params = Params(size: size, theme: theme, strings: strings, isActive: isActive) - if self.params == params { - return - } - self.params = params - self.update(params: params, transition: transition) - } - - private func update(params: Params, transition: ComponentTransition) { - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) - - self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) + public func update(size: CGSize, isDark: Bool, tintColor: GlassBackgroundView.TintColor, iconColor: UIColor, transition: ComponentTransition) { + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, tintColor: tintColor, transition: transition) if self.iconView.image == nil { self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate) } - transition.setTintColor(view: self.iconView, color: params.isActive ? params.theme.rootController.navigationSearchBar.inputIconColor : params.theme.chat.inputPanel.panelControlColor) + self.iconView.tintColor = iconColor if let image = self.iconView.image { - let imageSize: CGSize - let iconFrame: CGRect - if params.isActive { - let iconFraction: CGFloat = 0.8 - imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction) - iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((params.size.height - imageSize.height) * 0.5)), size: imageSize) - } else { - iconFrame = CGRect(origin: CGPoint(x: floor((params.size.width - image.size.width) * 0.5), y: floor((params.size.height - image.size.height) * 0.5)), size: image.size) - } - transition.setPosition(view: self.iconView, position: iconFrame.center) - transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) - } - - if params.isActive { - let searchBarNode: SearchBarNode - var searchBarNodeTransition = transition - if let current = self.searchBarNode { - searchBarNode = current - } else { - searchBarNodeTransition = searchBarNodeTransition.withAnimation(.none) - searchBarNode = SearchBarNode( - theme: SearchBarNodeTheme( - background: .clear, - separator: .clear, - inputFill: .clear, - primaryText: params.theme.chat.inputPanel.panelControlColor, - placeholder: params.theme.chat.inputPanel.inputPlaceholderColor, - inputIcon: params.theme.chat.inputPanel.inputControlColor, - inputClear: params.theme.chat.inputPanel.inputControlColor, - accent: params.theme.chat.inputPanel.panelControlAccentColor, - keyboard: params.theme.rootController.keyboardColor - ), - strings: params.strings, - fieldStyle: .inlineNavigation, - icon: .loupe, - forceSeparator: false, - displayBackground: false, - cancelText: nil - ) - searchBarNode.placeholderString = NSAttributedString(string: params.strings.Common_Search, font: Font.regular(17.0), textColor: params.theme.chat.inputPanel.inputPlaceholderColor) - self.searchBarNode = searchBarNode - self.backgroundView.contentView.addSubview(searchBarNode.view) - searchBarNode.view.alpha = 0.0 - } - let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: params.size.width - 36.0 - 4.0, height: params.size.height)) - transition.setFrame(view: searchBarNode.view, frame: searchBarFrame) - searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition) - alphaTransition.setAlpha(view: searchBarNode.view, alpha: 1.0) - } else { - if let searchBarNode = self.searchBarNode { - self.searchBarNode = nil - let searchBarNodeView = searchBarNode.view - alphaTransition.setAlpha(view: searchBarNode.view, alpha: 0.0, completion: { [weak searchBarNodeView] _ in - searchBarNodeView?.removeFromSuperview() - }) - } + transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)) } } } @@ -244,45 +131,20 @@ public final class TabBarComponent: Component { return true } } - - public final class Search: Equatable { - public let isActive: Bool - public let activate: () -> Void - public let deactivate: () -> Void - - public init(isActive: Bool, activate: @escaping () -> Void, deactivate: @escaping () -> Void) { - self.isActive = isActive - self.activate = activate - self.deactivate = deactivate - } - - public static func ==(lhs: Search, rhs: Search) -> Bool { - if lhs.isActive != rhs.isActive { - return false - } - return true - } - } public let theme: PresentationTheme - public let strings: PresentationStrings public let items: [Item] - public let search: Search? public let selectedId: AnyHashable? public let isTablet: Bool public init( theme: PresentationTheme, - strings: PresentationStrings, items: [Item], - search: Search?, selectedId: AnyHashable?, isTablet: Bool ) { self.theme = theme - self.strings = strings self.items = items - self.search = search self.selectedId = selectedId self.isTablet = isTablet } @@ -291,15 +153,9 @@ public final class TabBarComponent: Component { if lhs.theme !== rhs.theme { return false } - if lhs.strings !== rhs.strings { - return false - } if lhs.items != rhs.items { return false } - if lhs.search != rhs.search { - return false - } if lhs.selectedId != rhs.selectedId { return false } @@ -310,14 +166,11 @@ public final class TabBarComponent: Component { } public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate { - private let backgroundContainer: GlassBackgroundContainerView private let liquidLensView: LiquidLensView private let contextGestureContainerView: ContextControllerSourceView private var itemViews: [AnyHashable: ComponentView] = [:] private var selectedItemViews: [AnyHashable: ComponentView] = [:] - - private var searchView: NavigationSearchView? private var tabSelectionRecognizer: TabSelectionRecognizer? private var itemWithActiveContextGesture: AnyHashable? @@ -325,16 +178,11 @@ public final class TabBarComponent: Component { private var component: TabBarComponent? private weak var state: EmptyComponentState? - private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat)? private var overrideSelectedItemId: AnyHashable? - - public var currentSearchNode: ASDisplayNode? { - return self.searchView?.searchBarNode - } public override init(frame: CGRect) { - self.backgroundContainer = GlassBackgroundContainerView() - self.liquidLensView = LiquidLensView(useBackgroundContainer: false) + self.liquidLensView = LiquidLensView() self.contextGestureContainerView = ContextControllerSourceView() self.contextGestureContainerView.isGestureEnabled = true @@ -346,13 +194,12 @@ public final class TabBarComponent: Component { self.traitOverrides.horizontalSizeClass = .compact } - self.addSubview(self.backgroundContainer) - self.backgroundContainer.contentView.addSubview(self.contextGestureContainerView) + self.addSubview(self.contextGestureContainerView) self.contextGestureContainerView.addSubview(self.liquidLensView) let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) self.tabSelectionRecognizer = tabSelectionRecognizer - self.contextGestureContainerView.addGestureRecognizer(tabSelectionRecognizer) + self.addGestureRecognizer(tabSelectionRecognizer) self.contextGestureContainerView.shouldBegin = { [weak self] point in guard let self, let component = self.component else { @@ -434,41 +281,29 @@ public final class TabBarComponent: Component { } @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { - guard let component = self.component else { - return - } switch recognizer.state { case .began: - if let search = component.search, search.isActive { - } else if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view { + if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view { let startX = itemView.frame.minX - 4.0 - self.selectionGestureState = (startX, startX, itemId) + self.selectionGestureState = (startX, startX) self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } case .changed: - if let search = component.search, search.isActive { - } else if var selectionGestureState = self.selectionGestureState { + if var selectionGestureState = self.selectionGestureState { selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x - if let itemId = self.item(at: recognizer.location(in: self)) { - selectionGestureState.itemId = itemId - } self.selectionGestureState = selectionGestureState self.state?.updated(transition: .immediate, isLocal: true) } case .ended, .cancelled: - if let search = component.search, search.isActive { - search.deactivate() - } else if let selectionGestureState = self.selectionGestureState { - self.selectionGestureState = nil - if case .ended = recognizer.state, let component = self.component { - guard let item = component.items.first(where: { $0.id == selectionGestureState.itemId }) else { - return - } - self.overrideSelectedItemId = selectionGestureState.itemId - item.action(false) + self.selectionGestureState = nil + if let component = self.component, let itemId = self.item(at: recognizer.location(in: self)) { + guard let item = component.items.first(where: { $0.id == itemId }) else { + return } - self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) + self.overrideSelectedItemId = itemId + item.action(false) } + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) default: break } @@ -520,28 +355,22 @@ public final class TabBarComponent: Component { } func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) - let _ = alphaTransition - let innerInset: CGFloat = 4.0 let availableSize = CGSize(width: min(500.0, availableSize.width), height: availableSize.height) let previousComponent = self.component self.component = component self.state = state + + let _ = innerInset + let _ = availableSize + let _ = previousComponent self.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light - let barHeight: CGFloat = 56.0 + innerInset * 2.0 - - var availableItemsWidth: CGFloat = availableSize.width - innerInset * 2.0 - if component.search != nil { - availableItemsWidth -= barHeight + 8.0 - } - - let itemSize = CGSize(width: floor(availableItemsWidth / CGFloat(component.items.count)), height: 56.0) + let itemSize = CGSize(width: floor((availableSize.width - innerInset * 2.0) / CGFloat(component.items.count)), height: 56.0) let contentWidth: CGFloat = innerInset * 2.0 + CGFloat(component.items.count) * itemSize.width - let tabsSize = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0) + let size = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0) var validIds: [AnyHashable] = [] var selectionFrame: CGRect? @@ -580,7 +409,6 @@ public final class TabBarComponent: Component { component: AnyComponent(ItemComponent( item: item, theme: component.theme, - isCompact: component.search?.isActive == true, isSelected: false )), environment: {}, @@ -591,21 +419,14 @@ public final class TabBarComponent: Component { component: AnyComponent(ItemComponent( item: item, theme: component.theme, - isCompact: component.search?.isActive == true, isSelected: true )), environment: {}, containerSize: itemSize ) - var itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((tabsSize.height - itemSize.height) * 0.5)), size: itemSize) - if isItemSelected { - selectionFrame = itemFrame - } - + let itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((size.height - itemSize.height) * 0.5)), size: itemSize) if let itemComponentView = itemView.view as? ItemComponent.View, let selectedItemComponentView = selectedItemView.view as? ItemComponent.View { - let itemAlphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) - if itemComponentView.superview == nil { itemComponentView.isUserInteractionEnabled = false selectedItemComponentView.isUserInteractionEnabled = false @@ -613,27 +434,6 @@ public final class TabBarComponent: Component { self.liquidLensView.contentView.addSubview(itemComponentView) self.liquidLensView.selectedContentView.addSubview(selectedItemComponentView) } - - if let search = component.search, search.isActive { - if isItemSelected { - itemFrame.origin.x = floor((48.0 - itemSize.width) * 0.5) - itemTransition.setAlpha(view: itemComponentView, alpha: 1.0) - itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0) - itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0) - itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0) - } else { - itemTransition.setAlpha(view: itemComponentView, alpha: 0.0) - itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 10.0) - itemTransition.setAlpha(view: selectedItemComponentView, alpha: 0.0) - itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 10.0) - } - } else { - itemTransition.setAlpha(view: itemComponentView, alpha: 1.0) - itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0) - itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0) - itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0) - } - itemTransition.setFrame(view: itemComponentView, frame: itemFrame) itemTransition.setPosition(view: selectedItemComponentView, position: itemFrame.center) itemTransition.setBounds(view: selectedItemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) @@ -644,6 +444,9 @@ public final class TabBarComponent: Component { selectedItemComponentView.playSelectionAnimation() } } + if isItemSelected { + selectionFrame = itemFrame + } } var removeIds: [AnyHashable] = [] @@ -658,17 +461,12 @@ public final class TabBarComponent: Component { self.itemViews.removeValue(forKey: id) self.selectedItemViews.removeValue(forKey: id) } - - var tabsFrame = CGRect(origin: CGPoint(), size: tabsSize) - if let search = component.search, search.isActive { - tabsFrame.size = CGSize(width: 48.0, height: 48.0) - tabsFrame.origin.y = tabsSize.height - 48.0 - } - transition.setFrame(view: self.contextGestureContainerView, frame: tabsFrame) - transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: tabsSize)) + transition.setFrame(view: self.contextGestureContainerView, frame: CGRect(origin: CGPoint(), size: size)) + + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: size)) - var lensSelection: (x: CGFloat, width: CGFloat) + let lensSelection: (x: CGFloat, width: CGFloat) if let selectionGestureState = self.selectionGestureState { lensSelection = (selectionGestureState.currentX, itemSize.width + innerInset * 2.0) } else if let selectionFrame { @@ -677,62 +475,7 @@ public final class TabBarComponent: Component { lensSelection = (0.0, itemSize.width) } - var lensSize: CGSize = tabsSize - var isLensCollapsed = false - if let search = component.search, search.isActive { - isLensCollapsed = true - lensSize = CGSize(width: 48.0, height: 48.0) - lensSelection = (0.0, 48.0) - } - self.liquidLensView.update(size: lensSize, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition) - - var size = tabsSize - - if let search = component.search { - let searchSize: CGSize - let searchFrame: CGRect - if search.isActive { - size.width = availableSize.width - searchSize = CGSize(width: availableSize.width - 48.0 - 8.0, height: 48.0) - searchFrame = CGRect(origin: CGPoint(x: 48.0 + 8.0, y: size.height - searchSize.height), size: searchSize) - } else { - searchSize = CGSize(width: barHeight, height: barHeight) - size.width += barHeight + 8.0 - searchFrame = CGRect(origin: CGPoint(x: availableSize.width - searchSize.width, y: 0.0), size: searchSize) - } - - let searchView: NavigationSearchView - var searchViewTransition = transition - if let current = self.searchView { - searchView = current - } else { - searchViewTransition = searchViewTransition.withAnimation(.none) - searchView = NavigationSearchView(action: { [weak self] in - guard let self, let component = self.component else { - return - } - component.search?.activate() - }) - self.searchView = searchView - self.backgroundContainer.contentView.addSubview(searchView) - searchView.frame = CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchSize) - } - searchView.update(size: searchSize, theme: component.theme, strings: component.strings, isActive: search.isActive, transition: searchViewTransition) - transition.setFrame(view: searchView, frame: searchFrame) - } else { - if let searchView = self.searchView { - self.searchView = nil - transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchView.bounds.size), completion: { [weak searchView] completed in - guard let searchView, completed else { - return - } - searchView.removeFromSuperview() - }) - } - } - - transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundContainer.update(size: size, isDark: component.theme.overallDarkAppearance, transition: transition) + self.liquidLensView.update(size: size, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, transition: transition) return size } @@ -750,13 +493,11 @@ public final class TabBarComponent: Component { private final class ItemComponent: Component { let item: TabBarComponent.Item let theme: PresentationTheme - let isCompact: Bool let isSelected: Bool - init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool) { + init(item: TabBarComponent.Item, theme: PresentationTheme, isSelected: Bool) { self.item = item self.theme = theme - self.isCompact = isCompact self.isSelected = isSelected } @@ -767,9 +508,6 @@ private final class ItemComponent: Component { if lhs.theme !== rhs.theme { return false } - if lhs.isCompact != rhs.isCompact { - return false - } if lhs.isSelected != rhs.isSelected { return false } @@ -824,8 +562,6 @@ private final class ItemComponent: Component { } func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) - let previousComponent = self.component if previousComponent?.item.item !== component.item.item { @@ -883,7 +619,7 @@ private final class ItemComponent: Component { content: LottieComponent.AppBundleContent( name: animationName ), - color: (component.isSelected && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, + color: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, placeholderColor: nil, startingPosition: .end, size: CGSize(width: 48.0, height: 48.0), @@ -956,7 +692,6 @@ private final class ItemComponent: Component { self.contextContainerView.contentView.addSubview(titleView) } titleView.frame = titleFrame - alphaTransition.setAlpha(view: titleView, alpha: component.isCompact ? 0.0 : 1.0) } if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty { @@ -988,7 +723,6 @@ private final class ItemComponent: Component { self.contextContainerView.contentView.addSubview(badgeView) } badgeTransition.setFrame(view: badgeView, frame: badgeFrame) - alphaTransition.setAlpha(view: badgeView, alpha: component.isCompact ? 0.0 : 1.0) } } else if let badge = self.badge { self.badge = nil diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 2abea859eb..c0f1427eee 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -188,7 +188,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } public func addRootControllers(showCallsTab: Bool) { - let tabBarController = TabBarControllerImpl(theme: self.presentationData.theme, strings: self.presentationData.strings) + let tabBarController = TabBarControllerImpl(theme: self.presentationData.theme) tabBarController.navigationPresentation = .master let chatListController = self.context.sharedContext.makeChatListController(context: self.context, location: .chatList(groupId: .root), controlsHistoryPreload: true, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: !GlobalExperimentalSettings.isAppStoreBuild) if let sharedContext = self.context.sharedContext as? SharedAccountContextImpl { From d8b0878046e503899ea10a61141897f6711c3a39 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Mon, 1 Dec 2025 22:29:44 +0800 Subject: [PATCH 03/11] Update --- MODULE.bazel.lock | 6 +++--- .../MediaEditorScreen/Sources/MediaEditorScreen.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index a158c62a2c..ff6647515f 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -98,8 +98,8 @@ "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", - "https://bcr.bazel.build/modules/rules_java/8.12.0/MODULE.bazel": "8e6590b961f2defdfc2811c089c75716cb2f06c8a4edeb9a8d85eaa64ee2a761", - "https://bcr.bazel.build/modules/rules_java/8.12.0/source.json": "cbd5d55d9d38d4008a7d00bee5b5a5a4b6031fcd4a56515c9accbcd42c7be2ba", + "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615", + "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc", "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", @@ -156,7 +156,7 @@ "moduleExtensions": { "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { "general": { - "bzlTransitiveDigest": "hUTp2w+RUVdL7ma5esCXZJAFnX7vLbVfLd7FwnQI6bU=", + "bzlTransitiveDigest": "OlvsB0HsvxbR8ZN+J9Vf00X/+WVz/Y/5Xrq2LgcVfdo=", "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 71d2a6fedd..abc8a5cb1a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -3732,7 +3732,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { From 6bc1fe19395b96fcbdf4155c8104f25e4b2a01f1 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 1 Dec 2025 19:54:58 +0400 Subject: [PATCH 04/11] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 32 + .../Sources/AccountContext.swift | 1 + .../Sources/BotCheckoutActionButton.swift | 43 +- .../Sources/BotCheckoutControllerNode.swift | 16 +- .../Sources/BotCheckoutHeaderItem.swift | 2 +- .../Sources/BotCheckoutPriceItem.swift | 8 +- .../Sources/BotPaymentActionItemNode.swift | 10 +- .../Sources/BotPaymentCardInputItemNode.swift | 4 +- .../BotPaymentDisclosureItemNode.swift | 12 +- .../Sources/BotPaymentFieldItemNode.swift | 7 +- .../Sources/BotPaymentItemNode.swift | 2 +- .../Sources/BotPaymentSwitchItemNode.swift | 6 +- .../Sources/BotReceiptControllerNode.swift | 2 +- .../BrowserUI/Sources/BrowserWebContent.swift | 10 +- .../Sources/SheetComponent.swift | 76 +- .../GeneralChartComponentController.swift | 2 +- .../BarsComponentController.swift | 2 +- .../Sources/InviteLinkEditController.swift | 2 +- .../Sources/ListMessageItem.swift | 44 +- .../Sources/LocationPickerController.swift | 4 +- .../Sources/MediaPasteboardScreen.swift | 2 +- .../CreateExternalMediaStreamScreen/BUILD | 3 + .../CreateExternalMediaStreamScreen.swift | 291 ++-- .../ChannelPermissionsController.swift | 2 +- .../IncomingMessagePrivacyScreen.swift | 2 +- .../Sources/MonetizationBalanceItem.swift | 4 +- .../Sources/StatsOverviewItem.swift | 30 +- ...SyncCore_TranslationMessageAttribute.swift | 3 +- .../Messages/TelegramEngineMessages.swift | 2 +- .../TelegramEngine/Messages/Translate.swift | 45 +- .../Sources/ServiceMessageStrings.swift | 131 +- submodules/TelegramUI/BUILD | 1 + .../Sources/AttachmentFileController.swift | 290 +++- .../Sources/AttachmentFileSearchItem.swift | 95 +- .../Chat/ChatMessageBubbleItemNode/BUILD | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 53 +- .../ChatMessageGiftBubbleContentNode.swift | 15 +- .../BUILD | 36 + ...hatMessageGiftOfferBubbleContentNode.swift | 341 ++++ .../Chat/ChatMessageInteractiveFileNode/BUILD | 1 + .../ChatMessageInteractiveFileNode.swift | 56 +- .../Components/Chat/ChatQrCodeScreen/BUILD | 3 + .../Sources/ChatQrCodeScreen.swift | 184 ++- .../Sources/GiftCompositionComponent.swift | 18 +- .../Sources/GiftItemComponent.swift | 132 +- .../Components/Gifts/GiftViewScreen/BUILD | 1 + .../Sources/GiftAuctionBidScreen.swift | 53 +- .../Sources/GiftAuctionViewScreen.swift | 138 +- .../Sources/GiftOfferAlertController.swift | 485 ++++++ .../Sources/GiftTransferAlertController.swift | 2 +- .../Sources/GiftUnpinScreen.swift | 32 +- .../Sources/GiftUpgradePreviewScreen.swift | 1381 +++++++++++++++++ .../Sources/GiftValueScreen.swift | 2 +- .../Sources/GiftViewScreen.swift | 10 +- .../Sources/TableComponent.swift | 14 +- .../Components/MediaEditorScreen/BUILD | 2 + .../Sources/MediaEditorScreen.swift | 244 +-- .../PostSuggestionsSettingsScreen.swift | 2 +- .../Components/SegmentControlComponent/BUILD | 23 + .../Sources/SegmentControlComponent.swift | 21 +- .../Sources/StarsBalanceComponent.swift | 2 +- .../Sources/StarsOverviewItemComponent.swift | 2 +- .../Stars/StarsWithdrawalScreen/BUILD | 1 + .../Sources/StarsWithdrawalScreen.swift | 500 +++++- .../Components/StorageUsageScreen/BUILD | 1 + .../Sources/DataUsageScreen.swift | 1 + .../TelegramUI/Sources/ChatController.swift | 50 +- .../Sources/ChatHistoryListNode.swift | 10 +- .../Sources/SharedAccountContext.swift | 4 + .../TranslateUI/Sources/ChatTranslation.swift | 5 + .../Sources/WebAppSetEmojiStatusScreen.swift | 36 +- 71 files changed, 4405 insertions(+), 643 deletions(-) create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD create mode 100644 submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift create mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift create mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift create mode 100644 submodules/TelegramUI/Components/SegmentControlComponent/BUILD rename submodules/TelegramUI/Components/{StorageUsageScreen => SegmentControlComponent}/Sources/SegmentControlComponent.swift (92%) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d19b9a7112..7ce787238b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15434,3 +15434,35 @@ Error: %8$@"; "LiveStream.ErrorMaxAllowedEmoji.Text_any" = "You can send up to %d emoji."; "Stars.Purchase.StarGiftOfferInfo" = "Buy Stars to make a gift buy offer."; + +"Notification.StarsGiftOffer.Offer" = "%1$@ offered you %2$@ for your gift %3$@"; +"Notification.StarsGiftOffer.Offer.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Offer.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.OfferYou" = "You offered %1$@ %2$@ for their gift %3$@"; +"Notification.StarsGiftOffer.OfferYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.OfferYou.Stars_any" = "%@ Stars"; + +"Notification.StarsGiftOffer.Accepted" = "%1$@ accepted your offer and sold you %2$@ for %3$@"; +"Notification.StarsGiftOffer.Accepted.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Accepted.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.AcceptedYou" = "You sold %1$@ to %2$@ for %3$@"; +"Notification.StarsGiftOffer.AcceptedYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.AcceptedYou.Stars_any" = "%@ Stars"; + +"Notification.StarsGiftOffer.Rejected" = "%1$@ rejected your offer for %2$@ – your %3$@ have been refunded"; +"Notification.StarsGiftOffer.Rejected.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Rejected.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.RejectedYou" = "You rejected %1$@'s offer"; +"Notification.StarsGiftOffer.RejectedYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.RejectedYou.Stars_any" = "%@ Stars"; + +"Notification.StarsGiftOffer.Expired" = "%1$@ didn't respond to your offer for %2$@ within %3$@ – your %4$@ have been refunded"; +"Notification.StarsGiftOffer.Expired.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.Expired.Stars_any" = "%@ Stars"; +"Notification.StarsGiftOffer.ExpiredYou" = "The offer from %1$@ to buy your %2$@ for %3$@ has expired"; +"Notification.StarsGiftOffer.ExpiredYou.Stars_1" = "%@ Star"; +"Notification.StarsGiftOffer.ExpiredYou.Stars_any" = "%@ Stars"; + +"CreateExternalStream.RevokeStreamKey" = "Revoke Stream Key"; +"CreateExternalStream.Revoke.Text" = "Do you want to revoke the stream key?"; +"CreateExternalStream.Revoke.Revoke" = "Revoke"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5b2029e260..1a1aa391b2 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1427,6 +1427,7 @@ public protocol SharedAccountContext: AnyObject { func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void) -> ViewController func makeGiftAuctionActiveBidsScreen(context: AccountContext) -> ViewController + func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift index 1da8fd8434..e970e67688 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutActionButton.swift @@ -13,14 +13,14 @@ enum BotCheckoutActionButtonState: Equatable { private let titleFont = Font.semibold(17.0) -final class BotCheckoutActionButton: HighlightableButtonNode { +final class BotCheckoutActionButton: HighlightTrackingButtonNode { static var height: CGFloat = 52.0 private var activeFillColor: UIColor private var inactiveFillColor: UIColor private var foregroundColor: UIColor - private let activeBackgroundNode: ASImageNode + private let activeBackgroundNode: ASDisplayNode private var applePayButton: UIButton? private let labelNode: TextNode @@ -29,23 +29,17 @@ final class BotCheckoutActionButton: HighlightableButtonNode { private var placeholderNode: ShimmerEffectNode? - private var activeImage: UIImage? - private var inactiveImage: UIImage? - init(activeFillColor: UIColor, inactiveFillColor: UIColor, foregroundColor: UIColor) { self.activeFillColor = activeFillColor self.inactiveFillColor = inactiveFillColor self.foregroundColor = foregroundColor - let diameter: CGFloat = 20.0 - self.activeImage = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor) - self.inactiveImage = generateStretchableFilledCircleImage(diameter: diameter, color: inactiveFillColor) + let diameter: CGFloat = 52.0 - self.activeBackgroundNode = ASImageNode() - self.activeBackgroundNode.displaysAsynchronously = false - self.activeBackgroundNode.displayWithoutProcessing = true + self.activeBackgroundNode = ASDisplayNode() self.activeBackgroundNode.isLayerBacked = true - self.activeBackgroundNode.image = self.activeImage + self.activeBackgroundNode.backgroundColor = activeFillColor + self.activeBackgroundNode.cornerRadius = diameter / 2.0 self.labelNode = TextNode() self.labelNode.displaysAsynchronously = false @@ -55,6 +49,21 @@ final class BotCheckoutActionButton: HighlightableButtonNode { self.addSubnode(self.activeBackgroundNode) self.addSubnode(self.labelNode) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + let transition = ContainedViewLayoutTransition.animated(duration: highlighted ? 0.25 : 0.35, curve: .spring) + if highlighted { + let highlightedColor = self.activeFillColor.withMultiplied(hue: 1.0, saturation: 0.77, brightness: 1.01) + transition.updateBackgroundColor(node: self.activeBackgroundNode, color: highlightedColor) + transition.updateTransformScale(node: self, scale: 1.05) + } else { + transition.updateBackgroundColor(node: self.activeBackgroundNode, color: self.activeFillColor) + transition.updateTransformScale(node: self, scale: 1.0) + } + } } func setState(_ state: BotCheckoutActionButtonState) { @@ -93,14 +102,8 @@ final class BotCheckoutActionButton: HighlightableButtonNode { placeholderNode.removeFromSupernode() } - let image = isEnabled ? self.activeImage : self.inactiveImage - if let image = image, let currentImage = self.activeBackgroundNode.image, currentImage !== image { - self.activeBackgroundNode.image = image - self.activeBackgroundNode.layer.animate(from: currentImage.cgImage! as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2) - } else { - self.activeBackgroundNode.image = image - } - + self.activeBackgroundNode.backgroundColor = isEnabled ? self.activeFillColor : self.inactiveFillColor + let makeLayout = TextNode.asyncLayout(self.labelNode) let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift index 191f9b27c8..e81bf2e2a4 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutControllerNode.swift @@ -259,31 +259,31 @@ enum BotCheckoutEntry: ItemListNodeEntry { } }) case let .paymentMethod(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openPaymentMethod() }) case let .shippingInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.address(.street1)) }) case let .shippingMethod(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openShippingMethod() }) case let .nameInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.name) }) case let .emailInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.email) }) case let .phoneInfo(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: text, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { arguments.openInfo(.phone) }) case let .actionPlaceholder(_, shimmeringIndex): - return ItemListDisclosureItem(presentationData: presentationData, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, title: " ", label: " ", sectionId: self.section, style: .blocks, disclosureStyle: .none, action: { }, shimmeringIndex: shimmeringIndex) } } @@ -1245,7 +1245,7 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var updatedInsets = layout.intrinsicInsets - let bottomPanelHorizontalInset: CGFloat = 16.0 + let bottomPanelHorizontalInset: CGFloat = 30.0 var botName: String? if let botPeer = self.botPeerValue { diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift index 3104afe921..c2c4a9c922 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutHeaderItem.swift @@ -270,7 +270,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode { strongSelf.bottomStripeNode.isHidden = hasCorners } - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height - separatorHeight), size: CGSize(width: params.width, height: separatorHeight)) diff --git a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift index 745aa4162f..a88ecb09ed 100644 --- a/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift +++ b/submodules/BotPaymentsUI/Sources/BotCheckoutPriceItem.swift @@ -141,13 +141,13 @@ class BotCheckoutPriceItemNode: ListViewItemNode { let naturalContentHeight: CGFloat var verticalOffset: CGFloat = 0.0 if item.isFinal { - naturalContentHeight = 44.0 + naturalContentHeight = 52.0 } else { switch neighbors.bottom { case .otherSection, .none: - naturalContentHeight = 44.0 + naturalContentHeight = 52.0 default: - naturalContentHeight = 34.0 + naturalContentHeight = 42.0 } } if let _ = previousItem as? BotCheckoutHeaderItem { @@ -209,7 +209,7 @@ class BotCheckoutPriceItemNode: ListViewItemNode { strongSelf.separatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: params.width - leftInset, height: UIScreenPixel)) - strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil strongSelf.bottomSeparatorNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: contentSize.height), size: CGSize(width: params.width, height: UIScreenPixel)) diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift index dfba43615a..5d89bef1e7 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentActionItemNode.swift @@ -70,15 +70,17 @@ final class BotPaymentActionItemNode: BotPaymentItemNode { self.titleNode.attributedText = NSAttributedString(string: self.title, font: titleFont, textColor: theme.list.itemAccentColor) } - self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0)) - transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel))) + let height: CGFloat = 52.0 + + self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height)) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel))) let leftInset: CGFloat = 16.0 let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 32.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) - return 44.0 + return height } @objc func buttonPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift index f41dda6306..0e376a994e 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentCardInputItemNode.swift @@ -45,9 +45,9 @@ final class BotPaymentCardInputItemNode: BotPaymentItemNode, STPPaymentCardTextF self.cardField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance } - self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 0.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0)) + self.cardField.frame = CGRect(origin: CGPoint(x: 5.0 + sideInset, y: 4.0), size: CGSize(width: width - 10.0 - sideInset * 2.0, height: 44.0)) - return 44.0 + return 52.0 } func paymentCardTextFieldDidChange(_ textField: STPPaymentCardTextField) { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift index b482f85fea..6182eafb47 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentDisclosureItemNode.swift @@ -102,13 +102,15 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode { } } - self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: 44.0)) - transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: 44.0 + UIScreenPixel))) + let height: CGFloat = 52.0 + + self.buttonNode.frame = CGRect(origin: CGPoint(x: sideInset, y: 0.0), size: CGSize(width: width - sideInset * 2.0, height: height)) + transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height + UIScreenPixel))) let leftInset: CGFloat = 16.0 let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) var textInset = leftInset if !titleSize.width.isZero { @@ -117,9 +119,9 @@ class BotPaymentDisclosureItemNode: BotPaymentItemNode { textInset = max(measuredInset, textInset) let textSize = self.textNode.measure(CGSize(width: width - measuredInset - 8.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 11.0), size: textSize)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 16.0), size: textSize)) - return 44.0 + return height } @objc func buttonPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift index 41150436ee..eb3404884c 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentFieldItemNode.swift @@ -108,11 +108,12 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { self.textField.textField.tintColor = theme.list.itemAccentColor } + let height: CGFloat = 52.0 let leftInset: CGFloat = 16.0 let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) var textInset = leftInset if !titleSize.width.isZero { @@ -121,9 +122,9 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate { textInset = max(measuredInset, textInset) - transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 0.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0))) + transition.updateFrame(node: self.textField, frame: CGRect(origin: CGPoint(x: textInset + sideInset, y: 5.0), size: CGSize(width: max(1.0, width - textInset - 8.0), height: 40.0))) - return 44.0 + return height } func activateInput() { diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift index 38d57cefd4..15a03e7c97 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentItemNode.swift @@ -74,7 +74,7 @@ class BotPaymentItemNode: ASDisplayNode { self.bottomSeparatorNode.isHidden = hasCorners } - self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners) : nil + self.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(theme, top: hasTopCorners, bottom: hasBottomCorners, glass: true) : nil transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: height))) transition.updateFrame(node: self.maskNode, frame: self.backgroundNode.frame.insetBy(dx: sideInset, dy: 0.0)) diff --git a/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift b/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift index 5f582bfb9c..cffd44d2e9 100644 --- a/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotPaymentSwitchItemNode.swift @@ -74,14 +74,14 @@ final class BotPaymentSwitchItemNode: BotPaymentItemNode { let titleSize = self.titleNode.measure(CGSize(width: width - leftInset - 70.0 - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude)) - transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 11.0), size: titleSize)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: 16.0), size: titleSize)) let switchSize = self.switchNode.measure(CGSize(width: 100.0, height: 100.0)) - let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 6.0), size: switchSize) + let switchFrame = CGRect(origin: CGPoint(x: width - switchSize.width - 15.0 - sideInset, y: 12.0), size: switchSize) transition.updateFrame(node: self.switchNode, frame: switchFrame) transition.updateFrame(node: self.buttonNode, frame: switchFrame) - return 44.0 + return 52.0 } @objc private func buttonPressed() { diff --git a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift index 6100325d62..1d464bdcdf 100644 --- a/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift +++ b/submodules/BotPaymentsUI/Sources/BotReceiptControllerNode.swift @@ -335,7 +335,7 @@ final class BotReceiptControllerNode: ItemListControllerNode { override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, additionalInsets: UIEdgeInsets) { var updatedInsets = layout.intrinsicInsets - let bottomPanelHorizontalInset: CGFloat = 16.0 + let bottomPanelHorizontalInset: CGFloat = 30.0 let bottomPanelVerticalInset: CGFloat = 16.0 let bottomPanelHeight = max(updatedInsets.bottom, layout.inputHeight ?? 0.0) + bottomPanelVerticalInset * 2.0 + BotCheckoutActionButton.height diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 8fc6c4d6ce..20a3dbc59a 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -1425,9 +1425,13 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU var nodeList = document.getElementsByTagName('link'); for (var i = 0; i < nodeList.length; i++) { - if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon')||(nodeList[i].getAttribute('rel').startsWith('apple-touch-icon'))) - { - const node = nodeList[i]; + var rel = nodeList[i].getAttribute('rel') || ''; + if ( + rel === 'icon' || + rel === 'shortcut icon' || + rel.indexOf('apple-touch-icon') === 0 + ) { + var node = nodeList[i]; favicons.push({ url: node.getAttribute('href'), sizes: node.getAttribute('sizes') diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 94b2a3a2ba..2979b04e82 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -157,40 +157,6 @@ public final class SheetComponent: C } } - final class BackgroundView: UIView { - let topCornersView = UIView() - let bottomCornersView = UIView() - - override init(frame: CGRect) { - super.init(frame: frame) - - self.topCornersView.clipsToBounds = true - self.topCornersView.layer.cornerCurve = .continuous - self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] - - self.bottomCornersView.clipsToBounds = true - self.bottomCornersView.layer.cornerCurve = .continuous - self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - - self.addSubview(self.topCornersView) - self.topCornersView.addSubview(self.bottomCornersView) - } - - required init?(coder: NSCoder) { - preconditionFailure() - } - - func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) { - transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius) - transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius) - - transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size)) - transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size)) - - transition.setBackgroundColor(view: self.bottomCornersView, color: color) - } - } - public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { public final class Tag { public init() { @@ -208,9 +174,9 @@ public final class SheetComponent: C private let dimView: UIView private let scrollView: ScrollView - private let backgroundView: BackgroundView + private let backgroundView: SheetBackgroundView private var effectView: UIVisualEffectView? - private let clipView: BackgroundView + private let clipView: SheetBackgroundView private let contentView: ComponentView private var headerView: ComponentView? @@ -233,8 +199,8 @@ public final class SheetComponent: C self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceVertical = true - self.backgroundView = BackgroundView() - self.clipView = BackgroundView() + self.backgroundView = SheetBackgroundView() + self.clipView = SheetBackgroundView() self.contentView = ComponentView() @@ -589,3 +555,37 @@ public final class SheetComponent: C return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +public final class SheetBackgroundView: UIView { + let topCornersView = UIView() + let bottomCornersView = UIView() + + public override init(frame: CGRect) { + super.init(frame: frame) + + self.topCornersView.clipsToBounds = true + self.topCornersView.layer.cornerCurve = .continuous + self.topCornersView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + + self.bottomCornersView.clipsToBounds = true + self.bottomCornersView.layer.cornerCurve = .continuous + self.bottomCornersView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.addSubview(self.topCornersView) + self.topCornersView.addSubview(self.bottomCornersView) + } + + required init?(coder: NSCoder) { + preconditionFailure() + } + + public func update(size: CGSize, color: UIColor, topCornerRadius: CGFloat, bottomCornerRadius: CGFloat, transition: ComponentTransition) { + transition.setCornerRadius(layer: self.topCornersView.layer, cornerRadius: topCornerRadius) + transition.setCornerRadius(layer: self.bottomCornersView.layer, cornerRadius: bottomCornerRadius) + + transition.setFrame(view: self.topCornersView, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.bottomCornersView, frame: CGRect(origin: .zero, size: size)) + + transition.setBackgroundColor(view: self.bottomCornersView, color: color) + } +} diff --git a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift index 1b865e41ea..a6c32595ea 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/GeneralChartComponentController.swift @@ -371,7 +371,7 @@ class GeneralChartComponentController: ChartThemeContainer { values.append(ChartDetailsViewModel.Value( prefix: nil, title: self.strings.revenueInUsd, - value: "≈$\(convertedValueString)", + value: "~$\(convertedValueString)", color: color, visible: firstValue.visible )) diff --git a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift index 9f1339a6e6..1450e4bae6 100644 --- a/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift +++ b/submodules/GraphCore/Sources/Charts/Controllers/Stacked Bars/BarsComponentController.swift @@ -161,7 +161,7 @@ class BarsComponentController: GeneralChartComponentController { } else { text = String(format: "%0.3f", convertedValue) } - updatedLabels.append(LinesChartLabel(value: label.value, text: "≈$\(text)")) + updatedLabels.append(LinesChartLabel(value: label.value, text: "~$\(text)")) } labels = updatedLabels } diff --git a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift index 2ffe94c99b..13fc11eb8d 100644 --- a/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift +++ b/submodules/InviteLinksUI/Sources/InviteLinkEditController.swift @@ -494,7 +494,7 @@ private func inviteLinkEditControllerEntries(invite: ExportedInvitation?, state: var label: String = "" if let subscriptionFee = state.subscriptionFee, subscriptionFee > StarsAmount.zero { let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 - label = presentationData.strings.InviteLink_Create_FeePerMonth("≈\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string + label = presentationData.strings.InviteLink_Create_FeePerMonth("~\(formatTonUsdValue(subscriptionFee.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))").string } entries.append(.subscriptionFee(presentationData.theme, presentationData.strings.InviteLink_Create_FeePlaceholder, isEditingEnabled, state.subscriptionFee, label, StarsAmount(value: configuration.maxFee, nanos: 0))) } diff --git a/submodules/ListMessageItem/Sources/ListMessageItem.swift b/submodules/ListMessageItem/Sources/ListMessageItem.swift index a4c7ed9ba3..d017ac6395 100644 --- a/submodules/ListMessageItem/Sources/ListMessageItem.swift +++ b/submodules/ListMessageItem/Sources/ListMessageItem.swift @@ -44,7 +44,7 @@ public final class ListMessageItemInteraction { }) } -public final class ListMessageItem: ListViewItem { +public final class ListMessageItem: ListViewItem, ItemListItem { let presentationData: ChatPresentationData let systemStyle: ItemListSystemStyle let context: AccountContext @@ -57,6 +57,7 @@ public final class ListMessageItem: ListViewItem { let isGlobalSearchResult: Bool let isDownloadList: Bool let isSavedMusic: Bool + let isStoryMusic: Bool let displayFileInfo: Bool let displayBackground: Bool let canReorder: Bool @@ -64,9 +65,11 @@ public final class ListMessageItem: ListViewItem { let header: ListViewItemHeader? + public var sectionId: ItemListSectionId + public let selectable: Bool = true - public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain) { + public init(presentationData: ChatPresentationData, systemStyle: ItemListSystemStyle = .legacy, context: AccountContext, chatLocation: ChatLocation, interaction: ListMessageItemInteraction, message: Message?, translateToLanguage: String? = nil, selection: ChatHistoryMessageSelection, displayHeader: Bool, customHeader: ListViewItemHeader? = nil, hintIsLink: Bool = false, isGlobalSearchResult: Bool = false, isDownloadList: Bool = false, isSavedMusic: Bool = false, isStoryMusic: Bool = false, displayFileInfo: Bool = true, displayBackground: Bool = false, canReorder: Bool = false, style: ItemListStyle = .plain, sectionId: ItemListSectionId = 0) { self.presentationData = presentationData self.systemStyle = systemStyle self.context = context @@ -86,10 +89,12 @@ public final class ListMessageItem: ListViewItem { self.isGlobalSearchResult = isGlobalSearchResult self.isDownloadList = isDownloadList self.isSavedMusic = isSavedMusic + self.isStoryMusic = isStoryMusic self.displayFileInfo = displayFileInfo self.displayBackground = displayBackground self.canReorder = canReorder self.style = style + self.sectionId = sectionId } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { @@ -120,7 +125,23 @@ public final class ListMessageItem: ListViewItem { node.setupItem(self) let nodeLayout = node.asyncLayout() - let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem)) + + var topMerged = false + if let previousItem { + if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain { + topMerged = true + } + } + + var bottomMerged = false + if let nextItem { + if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain { + bottomMerged = true + } + } + + + let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem)) let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) node.updateSelectionState(animated: false) @@ -152,9 +173,22 @@ public final class ListMessageItem: ListViewItem { let nodeLayout = nodeValue.asyncLayout() + var topMerged = false + if let previousItem { + if let previousItem = previousItem as? ItemListItem, previousItem.sectionId == self.sectionId && !previousItem.isAlwaysPlain { + topMerged = true + } + } + + var bottomMerged = false + if let nextItem { + if let nextItem = nextItem as? ItemListItem, nextItem.sectionId == self.sectionId && !nextItem.isAlwaysPlain { + bottomMerged = true + } + } + async { - let (top, bottom, dateAtBottom) = (previousItem != nil && !(previousItem is ItemListItem), nextItem != nil, self.getDateAtBottom(top: previousItem, bottom: nextItem)) - + let (top, bottom, dateAtBottom) = (topMerged, bottomMerged, self.getDateAtBottom(top: previousItem, bottom: nextItem)) let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom) Queue.mainQueue().async { completion(layout, { _ in diff --git a/submodules/LocationUI/Sources/LocationPickerController.swift b/submodules/LocationUI/Sources/LocationPickerController.swift index 434b96002e..9942427e0e 100644 --- a/submodules/LocationUI/Sources/LocationPickerController.swift +++ b/submodules/LocationUI/Sources/LocationPickerController.swift @@ -446,9 +446,9 @@ public func storyLocationPickerController( ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) controller.requestController = { _, present in - let locationPickerController = LocationPickerController(context: context, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in + let locationPickerController = LocationPickerController(context: context, style: .glass, updatedPresentationData: updatedPresentationData, mode: .share(peer: nil, selfPeer: nil, hasLiveLocation: false), source: .story, initialLocation: location, completion: { location, queryId, resultId, address, countryCode in completion(location, queryId, resultId, address, countryCode) }) present(locationPickerController, locationPickerController.mediaPickerContext) diff --git a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift index e57222dfb6..dfe78fa1cc 100644 --- a/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift +++ b/submodules/MediaPasteboardUI/Sources/MediaPasteboardScreen.swift @@ -19,7 +19,7 @@ public func mediaPasteboardScreen( getSourceRect: (() -> CGRect?)? = nil, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil } ) -> ViewController { - let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: .peer(id: peer.id), buttons: [.standalone], initialButton: .standalone, makeEntityInputView: makeEntityInputView) controller.requestController = { _, present in presentMediaPicker(.media(subjects), false, nil, nil, { mediaPicker, mediaPickerContext in present(mediaPicker, mediaPickerContext) diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD index 768e2b7bd2..ff18627371 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/BUILD @@ -26,6 +26,9 @@ swift_library( "//submodules/Components/ActivityIndicatorComponent", "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/ListSectionComponent", + "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/PlainButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift index 939d80ae68..999385a0d6 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift @@ -14,6 +14,9 @@ import BundleIconComponent import AnimatedStickerComponent import ActivityIndicatorComponent import GlassBarButtonComponent +import ListSectionComponent +import ListActionItemComponent +import PlainButtonComponent private final class CreateExternalMediaStreamScreenComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -49,41 +52,51 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent final class State: ComponentState { let context: AccountContext let peerId: EnginePeer.Id + let mode: CreateExternalMediaStreamScreen.Mode private(set) var credentials: GroupCallStreamCredentials? var isDelayingLoadingIndication: Bool = true - private var credentialsDisposable: Disposable? + private let credentialsDisposable = MetaDisposable() private let activeActionDisposable = MetaDisposable() - init(context: AccountContext, peerId: EnginePeer.Id, credentialsPromise: Promise?) { + init(context: AccountContext, peerId: EnginePeer.Id, mode: CreateExternalMediaStreamScreen.Mode, credentialsPromise: Promise?) { self.context = context self.peerId = peerId + self.mode = mode super.init() + self.getCredentials(credentialsPromise: credentialsPromise) + } + + deinit { + self.credentialsDisposable.dispose() + self.activeActionDisposable.dispose() + } + + func getCredentials(credentialsPromise: Promise? = nil, revoke: Bool = false) { let credentialsSignal: Signal if let credentialsPromise = credentialsPromise { credentialsSignal = credentialsPromise.get() } else { - credentialsSignal = context.engine.calls.getGroupCallStreamCredentials(peerId: peerId, isLiveStream: false, revokePreviousCredentials: false) + var isLiveStream = false + if case let .create(isLiveStreamValue) = self.mode { + isLiveStream = isLiveStreamValue + } + credentialsSignal = self.context.engine.calls.getGroupCallStreamCredentials(peerId: self.peerId, isLiveStream: isLiveStream, revokePreviousCredentials: revoke) |> `catch` { _ -> Signal in return .never() } } - self.credentialsDisposable = (credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in + self.credentialsDisposable.set((credentialsSignal |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } strongSelf.credentials = result strongSelf.updated(transition: .immediate) - }) - } - - deinit { - self.credentialsDisposable?.dispose() - self.activeActionDisposable.dispose() + })) } func copyCredentials(_ key: KeyPath) { @@ -160,7 +173,7 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent } func makeState() -> State { - return State(context: self.context, peerId: self.peerId, credentialsPromise: self.credentialsPromise) + return State(context: self.context, peerId: self.peerId, mode: self.mode, credentialsPromise: self.credentialsPromise) } static var body: Body { @@ -176,26 +189,25 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let activityIndicator = Child(ActivityIndicatorComponent.self) - let credentialsBackground = Child(RoundedRectangle.self) + let credentialsSection = Child(ListSectionComponent.self) - let credentialsStripe = Child(Rectangle.self) - let credentialsURLTitle = Child(MultilineTextComponent.self) - let credentialsURLText = Child(MultilineTextComponent.self) - - let credentialsKeyTitle = Child(MultilineTextComponent.self) - let credentialsKeyText = Child(MultilineTextComponent.self) - - let credentialsCopyURLButton = Child(Button.self) - let credentialsCopyKeyButton = Child(Button.self) +// let credentialsBackground = Child(RoundedRectangle.self) +// let credentialsStripe = Child(Rectangle.self) +// let credentialsURLTitle = Child(MultilineTextComponent.self) +// let credentialsURLText = Child(MultilineTextComponent.self) +// +// let credentialsKeyTitle = Child(MultilineTextComponent.self) +// let credentialsKeyText = Child(MultilineTextComponent.self) +// +// let credentialsCopyURLButton = Child(Button.self) +// let credentialsCopyKeyButton = Child(Button.self) return { context in let topInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0 let buttonSideInset: CGFloat = 36.0 - let credentialsSideInset: CGFloat = 16.0 - let credentialsTopInset: CGFloat = 12.0 - let credentialsTitleSpacing: CGFloat = 5.0 + let component = context.component let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let state = context.state @@ -355,124 +367,145 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent ) let textFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - text.size.width) / 2.0), y: animationFrame.maxY + 18.0), size: text.size) - context.add(text .position(CGPoint(x: textFrame.midX, y: textFrame.midY)) ) - let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize) - if let credentials = context.state.credentials { - let credentialsURLTitle = credentialsURLTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_ServerUrl, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height), - transition: context.transition + var credentialsSectionItems: [AnyComponentWithIdentity] = [] + credentialsSectionItems.append( + AnyComponentWithIdentity(id: "url", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreateExternalStream_ServerUrl, + font: Font.regular(15.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), + horizontalAlignment: .left, + truncationType: .middle, + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 5.0)), + contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "copy", + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), + action: { [weak state] in + guard let state = state else { + return + } + state.copyCredentials(\.url) + }, + animateScale: false + ) + ) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) ) - let credentialsKeyTitle = credentialsKeyTitle.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_StreamKey, font: Font.regular(15.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: credentialsAreaSize.height), - transition: context.transition + credentialsSectionItems.append( + AnyComponentWithIdentity(id: "key", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.CreateExternalStream_StreamKey, + font: Font.regular(15.0), + textColor: theme.list.itemPrimaryTextColor + )), + maximumNumberOfLines: 1 + ))), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), + horizontalAlignment: .left, + truncationType: .middle, + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 5.0)), + contentInsets: UIEdgeInsets(top: 14.0, left: 0.0, bottom: 14.0, right: 0.0), + accessory: .custom(ListActionItemComponent.CustomAccessory( + component: AnyComponentWithIdentity( + id: "copy", + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), + action: { [weak state] in + guard let state = state else { + return + } + state.copyCredentials(\.streamKey) + }, + animateScale: false + ) + ) + ), + insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 14.0), + isInteractive: true + )), + action: nil + ) + )) ) - let credentialsURLText = credentialsURLText.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: credentials.url, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - truncationType: .middle, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 22.0, height: credentialsAreaSize.height), - transition: context.transition - ) - - let credentialsKeyText = credentialsKeyText.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: credentials.streamKey, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .left)), - horizontalAlignment: .left, - truncationType: .middle, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0 - 48.0, height: credentialsAreaSize.height), - transition: context.transition - ) - - let credentialsBackground = credentialsBackground.update( - component: RoundedRectangle(color: theme.list.itemBlocksBackgroundColor, cornerRadius: 26.0), - availableSize: credentialsAreaSize, - transition: context.transition - ) - - let credentialsStripe = credentialsStripe.update( - component: Rectangle(color: theme.list.itemPlainSeparatorColor), - availableSize: CGSize(width: credentialsAreaSize.width - credentialsSideInset * 2.0, height: UIScreenPixel), - transition: context.transition - ) - - let credentialsCopyURLButton = credentialsCopyURLButton.update( - component: Button( - content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), - action: { [weak state] in - guard let state = state else { - return + credentialsSectionItems.append( + AnyComponentWithIdentity(id: "revoke", component: AnyComponent( + ListActionItemComponent( + theme: theme, + style: .glass, + title: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: environment.strings.CreateExternalStream_RevokeStreamKey, font: Font.regular(17.0), textColor: theme.list.itemDestructiveColor)), + horizontalAlignment: .center, + truncationType: .middle, + maximumNumberOfLines: 1 + )), + titleAlignment: .center, + action: { [weak state] _ in + guard let state = state else { + return + } + let alertController = textAlertController(context: component.context, title: nil, text: environment.strings.CreateExternalStream_Revoke_Text, actions: [TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: { + }), TextAlertAction(type: .defaultAction, title: environment.strings.CreateExternalStream_Revoke_Revoke, action: { [weak state] in + state?.getCredentials(revoke: true) + })]) + environment.controller()?.present(alertController, in: .window(.root)) } - state.copyCredentials(\.url) - } - ).minSize(CGSize(width: 44.0, height: 44.0)), - availableSize: CGSize(width: 44.0, height: 44.0), + ) + )) + ) + + let credentialsSection = credentialsSection.update( + component: ListSectionComponent( + theme: theme, + style: .glass, + header: nil, + footer: nil, + items: credentialsSectionItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), transition: context.transition ) - - let credentialsCopyKeyButton = credentialsCopyKeyButton.update( - component: Button( - content: AnyComponent(BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: theme.list.itemAccentColor)), - action: { [weak state] in - guard let state = state else { - return - } - state.copyCredentials(\.streamKey) - } - ).minSize(CGSize(width: 44.0, height: 44.0)), - availableSize: CGSize(width: 44.0, height: 44.0), - transition: context.transition - ) - - context.add(credentialsBackground - .position(CGPoint(x: credentialsFrame.midX, y: credentialsFrame.midY)) - ) - - context.add(credentialsStripe - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsStripe.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight)) - ) - - context.add(credentialsURLTitle - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsURLTitle.size.height / 2.0)) - ) - context.add(credentialsURLText - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsURLText.size.width / 2.0, y: credentialsFrame.minY + credentialsTopInset + credentialsTitleSpacing + credentialsURLTitle.size.height + credentialsURLText.size.height / 2.0)) - ) - context.add(credentialsCopyURLButton - .position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyURLButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight / 2.0)) - ) - - context.add(credentialsKeyTitle - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyTitle.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsKeyTitle.size.height / 2.0)) - ) - context.add(credentialsKeyText - .position(CGPoint(x: credentialsFrame.minX + credentialsSideInset + credentialsKeyText.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsTopInset + credentialsTitleSpacing + credentialsKeyTitle.size.height + credentialsKeyText.size.height / 2.0)) - ) - context.add(credentialsCopyKeyButton - .position(CGPoint(x: credentialsFrame.maxX - 4.0 - credentialsCopyKeyButton.size.width / 2.0, y: credentialsFrame.minY + credentialsItemHeight + credentialsItemHeight / 2.0)) - ) + context.add(credentialsSection + .position(CGPoint(x: context.availableSize.width / 2.0, y: textFrame.maxY + 30.0 + credentialsSection.size.height / 2.0))) } else if !context.state.isDelayingLoadingIndication { + let credentialsFrame = CGRect(origin: CGPoint(x: sideInset, y: textFrame.maxY + 30.0), size: credentialsAreaSize) let activityIndicator = activityIndicator.update( component: ActivityIndicatorComponent(color: theme.list.controlSecondaryColor), availableSize: CGSize(width: 100.0, height: 100.0), diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index df917543dc..5e130739d4 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -732,7 +732,7 @@ private func channelPermissionsControllerEntries(context: AccountContext, presen var price: String = "" let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 - price = "≈\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + price = "~\(formatTonUsdValue(sendPaidMessageStars, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" entries.append(.messagePriceHeader(presentationData.theme, presentationData.strings.GroupInfo_Permissions_MessagePrice)) entries.append(.messagePrice(presentationData.theme, sendPaidMessageStars, configuration.paidMessageMaxAmount, price)) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift index c99013d06d..52ec1c3c80 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/IncomingMessagePrivacyScreen.swift @@ -194,7 +194,7 @@ private func incomingMessagePrivacyScreenEntries(presentationData: PresentationD let usdRate = Double(configuration.usdWithdrawRate) / 1000.0 / 100.0 - let price = "≈\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + let price = "~\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" entries.append(.price(value: amount.value, maxValue: configuration.paidMessageMaxAmount, price: price, isEnabled: isPremium)) entries.append(.priceInfo(commission: configuration.paidMessageCommissionPermille / 10, value: price)) diff --git a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift index 3c9d2c4f20..98441dc464 100644 --- a/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift +++ b/submodules/StatisticsUI/Sources/MonetizationBalanceItem.swift @@ -180,10 +180,10 @@ final class MonetizationBalanceItemNode: ListViewItemNode, ItemListItemNode { case .ton: let cryptoValue = formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat) amountString = tonAmountAttributedString(cryptoValue, integralFont: integralFont, fractionalFont: fractionalFont, color: item.presentationData.theme.list.itemPrimaryTextColor, decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) - value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" + value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" case .stars: amountString = NSAttributedString(string: presentationStringsFormattedNumber(stats.balances.availableBalance.amount, item.presentationData.dateTimeFormat.groupingSeparator), font: integralFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor) - value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" + value = stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))" isStars = true } } else { diff --git a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift index 4a707b0696..a5b9d6d2d2 100644 --- a/submodules/StatisticsUI/Sources/StatsOverviewItem.swift +++ b/submodules/StatisticsUI/Sources/StatsOverviewItem.swift @@ -443,7 +443,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–", + item.publicShares.flatMap { "~\( compactNumericCountString(max(0, stats.forwards - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, nil, .generic @@ -485,7 +485,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - item.publicShares.flatMap { "≈\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–", + item.publicShares.flatMap { "~\( compactNumericCountString(max(0, views.forwardCount - Int($0))))" } ?? "–", item.presentationData.strings.Stats_Message_PrivateShares, nil, .generic @@ -512,7 +512,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.context, params.width, item.presentationData, - "≈\(Int(stats.premiumAudience?.value ?? 0))", + "~\(Int(stats.premiumAudience?.value ?? 0))", item.isGroup ? item.presentationData.strings.Stats_Boosts_PremiumMembers : item.presentationData.strings.Stats_Boosts_PremiumSubscribers, (String(format: "%.02f%%", premiumSubscribers * 100.0), .generic), .generic @@ -770,7 +770,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Available, - (stats.balances.availableBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -780,7 +780,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Current, - (stats.balances.currentBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -790,7 +790,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Total, - (stats.balances.overallRevenue.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -800,7 +800,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(additionalStats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), " ", - (additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (additionalStats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.availableBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -810,7 +810,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(additionalStats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), " ", - (additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (additionalStats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.currentBalance.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -820,7 +820,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(additionalStats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat), " ", - (additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (additionalStats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(additionalStats.balances.overallRevenue.amount.value, divide: false, rate: additionalStats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -834,7 +834,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.availableBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Available, - (stats.balances.availableBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.availableBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -844,7 +844,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.currentBalance.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Current, - (stats.balances.currentBalance.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.currentBalance.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -854,7 +854,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatTonAmountText(stats.balances.overallRevenue.amount.value, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_Overview_Total, - (stats.balances.overallRevenue.amount.value == 0 ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.overallRevenue.amount.value == 0 ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .ton ) @@ -869,7 +869,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(stats.balances.availableBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Available, - (stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.availableBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.availableBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -879,7 +879,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(stats.balances.currentBalance.amount, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Current, - (stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.currentBalance.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.currentBalance.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) @@ -889,7 +889,7 @@ class StatsOverviewItemNode: ListViewItemNode { item.presentationData, formatStarsAmountText(stats.balances.overallRevenue.amount, dateTimeFormat: item.presentationData.dateTimeFormat), item.presentationData.strings.Monetization_StarsProceeds_Total, - (stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "≈\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), + (stats.balances.overallRevenue.amount == StarsAmount.zero ? "" : "~\(formatTonUsdValue(stats.balances.overallRevenue.amount.value, divide: false, rate: stats.usdRate, dateTimeFormat: item.presentationData.dateTimeFormat))", .generic), .stars ) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift index 5239cae769..37333a5dc7 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TranslationMessageAttribute.swift @@ -25,10 +25,9 @@ public class TranslationMessageAttribute: MessageAttribute, Equatable { public let entities: [MessageTextEntity] public let toLang: String - public let additional:[Additional] + public let additional: [Additional] public let pollSolution: Additional? - public var associatedPeerIds: [PeerId] { return [] } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 2bad40a011..9133c0a5ac 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -593,7 +593,7 @@ public extension TelegramEngine { } public func translate(texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { - return _internal_translate_texts(network: self.account.network, texts: texts, toLang: toLang) + return _internal_translateTexts(network: self.account.network, texts: texts, toLang: toLang) } public func translateMessages(messageIds: [EngineMessage.Id], fromLang: String?, toLang: String, enableLocalIfPossible: Bool) -> Signal { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift index aaaaed5e05..28574d329d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Translate.swift @@ -48,7 +48,7 @@ func _internal_translate(network: Network, text: String, toLang: String, entitie } } -func _internal_translate_texts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { +func _internal_translateTexts(network: Network, texts: [(String, [MessageTextEntity])], toLang: String) -> Signal<[(String, [MessageTextEntity])], TranslationError> { var flags: Int32 = 0 flags |= (1 << 1) @@ -112,9 +112,9 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin return .never() } - let polls = messages.compactMap { msg in - if let poll = msg.media.first as? TelegramMediaPoll { - return (poll, msg.id) + let polls = messages.compactMap { message in + if let poll = message.media.first as? TelegramMediaPoll { + return (poll, message.id) } else { return nil } @@ -128,9 +128,19 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin if let solution = poll.results.solution { texts.append((solution.text, solution.entities)) } - return _internal_translate_texts(network: account.network, texts: texts, toLang: toLang) + return _internal_translateTexts(network: account.network, texts: texts, toLang: toLang) } + let audioTranscriptions = messages.compactMap { message in + if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending { + return (audioTranscription.text, message.id) + } else { + return nil + } + } + let audioTranscriptionsSignals = audioTranscriptions.map { (text, id) in + return _internal_translate(network: account.network, text: text, toLang: toLang) + } var flags: Int32 = 0 flags |= (1 << 0) @@ -197,8 +207,8 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin } } - return combineLatest(msgs, combineLatest(pollSignals)) - |> mapToSignal { (result, pollResults) -> Signal in + return combineLatest(msgs, combineLatest(pollSignals), combineLatest(audioTranscriptionsSignals)) + |> mapToSignal { (result, pollResults, audioTranscriptionsResults) -> Signal in return account.postbox.transaction { transaction in if case let .translateResult(results) = result { var index = 0 @@ -218,6 +228,7 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin index += 1 } } + if !pollResults.isEmpty { for (i, poll) in polls.enumerated() { let result = pollResults[i] @@ -231,12 +242,12 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin if translated.0.isEmpty { translated = (poll.0.options[i].text, poll.0.options[i].entities) } - attrOptions.append(.init(text: translated.0, entities: translated.1)) + attrOptions.append(TranslationMessageAttribute.Additional(text: translated.0, entities: translated.1)) } let solution: TranslationMessageAttribute.Additional? if result.count > 1 + poll.0.options.count, !result[result.count - 1].0.isEmpty { - solution = .init(text: result[result.count - 1].0, entities: result[result.count - 1].1) + solution = TranslationMessageAttribute.Additional(text: result[result.count - 1].0, entities: result[result.count - 1].1) } else { solution = nil } @@ -251,6 +262,22 @@ private func _internal_translateMessagesByPeerId(account: Account, peerId: Engin } } } + + if !audioTranscriptionsResults.isEmpty { + for (i, audioTranscription) in audioTranscriptions.enumerated() { + if let result = audioTranscriptionsResults[i] { + transaction.updateMessage(audioTranscription.1, update: { currentMessage in + let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init) + var attributes = currentMessage.attributes.filter { !($0 is TranslationMessageAttribute) } + + let updatedAttribute: TranslationMessageAttribute = TranslationMessageAttribute(text: result.0, entities: result.1, additional: [], pollSolution: nil, toLang: toLang) + attributes.append(updatedAttribute) + + return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media)) + }) + } + } + } } |> castError(TranslationError.self) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 8c972d6962..ed139e7eb1 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1262,7 +1262,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, starsPrice)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, _, isPrepaidUpgrade, peerId, senderId, _, resaleStars, _, _, _, assigned, fromOffer): if case let .unique(gift) = gift { if !forAdditionalServiceMessage && !"".isEmpty { attributedString = NSAttributedString(string: "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))", font: titleFont, textColor: primaryTextColor) @@ -1290,7 +1290,33 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, } } } else { - if message.id.peerId == accountPeerId && assigned { + if fromOffer, let resaleStars { + let starsString: String + switch resaleStars.currency { + case .stars: + starsString = strings.Notification_StarsGiftOffer_Accepted_Stars(Int32(clamping: resaleStars.amount.value)) + case .ton: + starsString = formatTonAmountText(resaleStars.amount.value, dateTimeFormat: dateTimeFormat) + " TON" + } + let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" + var peerName = "" + if let name = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle { + peerName = name + } + if message.author?.id == accountPeerId { + let peerIds: [(Int, EnginePeer.Id?)] = [(1, message.id.peerId)] + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[0] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_AcceptedYou(giftTitle, peerName, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.author?.id)] + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Accepted(peerName, giftTitle, starsString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } else if message.id.peerId == accountPeerId && assigned { let attributes: [Int: MarkdownAttributeSet] = [0: boldAttributes] let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Assigned(giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes) @@ -1616,10 +1642,103 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGift_Sent(authorName, price)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } - case .starGiftPurchaseOffer: - attributedString = nil - case .starGiftPurchaseOfferDeclined: - attributedString = nil + case let .starGiftPurchaseOffer(gift, amount, _, _, _): + let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + + let giftTitle: String + if case let .unique(gift) = gift { + giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))" + } else { + giftTitle = "" + } + + let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)] + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + + if message.author?.id == accountPeerId { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_OfferYou_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_OfferYou(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_Offer_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Offer(peerName, priceString, giftTitle)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + case let .starGiftPurchaseOfferDeclined(gift, amount, hasExpired): + let peerName = message.peers[message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + let peerIds: [(Int, EnginePeer.Id?)] = [(0, message.id.peerId)] + + let giftTitle: String + if case let .unique(gift) = gift { + giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: dateTimeFormat))" + } else { + giftTitle = "" + } + + if hasExpired { + if message.author?.id == accountPeerId { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_ExpiredYou_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_ExpiredYou(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_Expired_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + let timeString = "[TODO]" + + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributes[3] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Expired(peerName, giftTitle, timeString, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } else { + if message.author?.id == accountPeerId { + let attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_RejectedYou(peerName)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } else { + let priceString: String + switch amount.currency { + case .stars: + priceString = strings.Notification_StarsGiftOffer_Rejected_Stars(Int32(clamping: amount.amount.value)) + case .ton: + priceString = "\(amount.amount) TON" + } + + var attributes = peerMentionsAttributes(primaryTextColor: primaryTextColor, peerIds: peerIds) + attributes[1] = boldAttributes + attributes[2] = boldAttributes + attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Rejected(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes) + } + } case .unknown: attributedString = nil } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 83a0f17408..cb1458551a 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -402,6 +402,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode", "//submodules/TelegramUI/Components/Chat/ChatRecentActionsController", diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift index 07c787129a..e3af2cbb87 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileController.swift @@ -18,23 +18,29 @@ import ComponentFlow import GlassBarButtonComponent import BundleIconComponent import EdgeEffect +import SaveToCameraRoll private final class AttachmentFileControllerArguments { let context: AccountContext + let isAudio: Bool let openGallery: () -> Void let openFiles: () -> Void + let expandSavedMusic: () -> Void let send: (Message) -> Void - init(context: AccountContext, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, send: @escaping (Message) -> Void) { + init(context: AccountContext, isAudio: Bool, openGallery: @escaping () -> Void, openFiles: @escaping () -> Void, expandSavedMusic: @escaping () -> Void, send: @escaping (Message) -> Void) { self.context = context + self.isAudio = isAudio self.openGallery = openGallery self.openFiles = openFiles + self.expandSavedMusic = expandSavedMusic self.send = send } } private enum AttachmentFileSection: Int32 { case select + case savedMusic case recent } @@ -55,6 +61,10 @@ private enum AttachmentFileEntry: ItemListNodeEntry { case selectFromGallery(PresentationTheme, String) case selectFromFiles(PresentationTheme, String) + case savedHeader(PresentationTheme, String) + case savedFile(Int32, PresentationTheme, Message?) + case showMore(PresentationTheme, String) + case recentHeader(PresentationTheme, String) case file(Int32, PresentationTheme, Message?) @@ -62,6 +72,8 @@ private enum AttachmentFileEntry: ItemListNodeEntry { switch self { case .selectFromGallery, .selectFromFiles: return AttachmentFileSection.select.rawValue + case .savedHeader, .savedFile, .showMore: + return AttachmentFileSection.savedMusic.rawValue case .recentHeader, .file: return AttachmentFileSection.recent.rawValue } @@ -73,10 +85,16 @@ private enum AttachmentFileEntry: ItemListNodeEntry { return 0 case .selectFromFiles: return 1 - case .recentHeader: + case .savedHeader: return 2 - case let .file(index, _, _): + case let .savedFile(index, _, _): return 3 + index + case .showMore: + return 9999 + case .recentHeader: + return 10000 + case let .file(index, _, _): + return 10001 + index } } @@ -94,6 +112,24 @@ private enum AttachmentFileEntry: ItemListNodeEntry { } else { return false } + case let .savedHeader(lhsTheme, lhsText): + if case let .savedHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } + case let .savedFile(lhsIndex, lhsTheme, lhsMessage): + if case let .savedFile(rhsIndex, rhsTheme, rhsMessage) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, areMessagesEqual(lhsMessage, rhsMessage) { + return true + } else { + return false + } + case let .showMore(lhsTheme, lhsText): + if case let .showMore(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { + return true + } else { + return false + } case let .recentHeader(lhsTheme, lhsText): if case let .recentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true @@ -124,6 +160,22 @@ private enum AttachmentFileEntry: ItemListNodeEntry { return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.cloudIcon(presentationData.theme), title: text, alwaysPlain: false, sectionId: self.section, height: .generic, editing: false, action: { arguments.openFiles() }) + + case let .savedHeader(_, text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .savedFile(_, _, message): + let interaction = ListMessageItemInteraction(openMessage: { message, _ in + arguments.send(message) + return false + }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) + + let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat + let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false)) + return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: arguments.context.account.peerId), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) + case let .showMore(theme, text): + return ItemListPeerActionItem(presentationData: presentationData, systemStyle: .glass, icon: PresentationResourcesItemList.downArrowImage(theme), title: text, sectionId: self.section, editing: false, action: { + arguments.expandSavedMusic() + }) case let .recentHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .file(_, _, message): @@ -135,22 +187,53 @@ private enum AttachmentFileEntry: ItemListNodeEntry { let dateTimeFormat = arguments.context.sharedContext.currentPresentationData.with({$0}).dateTimeFormat let chatPresentationData = ChatPresentationData(theme: ChatPresentationThemeData(theme: presentationData.theme, wallpaper: .color(0)), fontSize: presentationData.fontSize, strings: presentationData.strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: .firstLast, disableAnimations: false, largeEmoji: false, chatBubbleCorners: PresentationChatBubbleCorners(mainRadius: 0, auxiliaryRadius: 0, mergeBubbleCorners: false)) - return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, displayFileInfo: false, displayBackground: true, style: .blocks) + return ListMessageItem(presentationData: chatPresentationData, systemStyle: .glass, context: arguments.context, chatLocation: .peer(id: PeerId(0)), interaction: interaction, message: message, selection: .none, displayHeader: false, isDownloadList: arguments.isAudio, isStoryMusic: true, displayFileInfo: true, displayBackground: true, style: .blocks, sectionId: self.section) } } } -private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] { +private func attachmentFileControllerEntries(presentationData: PresentationData, mode: AttachmentFileControllerMode, state: AttachmentFileControllerState, savedMusic: [Message]?, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] { guard !empty else { return [] } var entries: [AttachmentFileEntry] = [] - entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery)) + if case .recent = mode { + entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery)) + } entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles)) + let listTitle: String + switch mode { + case .recent: + listTitle = presentationData.strings.Attachment_RecentlySentFiles + case .audio: + //TODO:localize + listTitle = "SHARED AUDIO" + } + + if case .audio = mode { + if let savedMusic, savedMusic.count > 0 { + entries.append(.savedHeader(presentationData.theme, "SAVED MUSIC".uppercased())) + var savedMusic = savedMusic + var showMore = false + if savedMusic.count > 4 && !state.savedMusicExpanded { + savedMusic = Array(savedMusic.prefix(3)) + showMore = true + } + var i: Int32 = 0 + for file in savedMusic { + entries.append(.savedFile(i, presentationData.theme, file)) + i += 1 + } + if showMore { + entries.append(.showMore(presentationData.theme, "Show More")) + } + } + } + if let recentDocuments = recentDocuments { if recentDocuments.count > 0 { - entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased())) + entries.append(.recentHeader(presentationData.theme, listTitle.uppercased())) var i: Int32 = 0 for file in recentDocuments { entries.append(.file(i, presentationData.theme, file)) @@ -158,7 +241,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData, } } } else { - entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased())) + entries.append(.recentHeader(presentationData.theme, listTitle.uppercased())) for i in 0 ..< 11 { entries.append(.file(Int32(i), presentationData.theme, nil)) } @@ -185,6 +268,8 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon var delayDisappear = false + var hasBottomEdgeEffect = true + var resetForReuseImpl: () -> Void = {} public func resetForReuse() { self.resetForReuseImpl() @@ -229,31 +314,41 @@ public class AttachmentFileControllerImpl: ItemListController, AttachmentFileCon transition.updateFrame(view: topEdgeEffectView, frame: topEdgeEffectFrame) topEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: topEdgeEffectFrame, edge: .top, edgeSize: topEdgeEffectFrame.height, transition: ComponentTransition(transition)) - let bottomEdgeEffectView: EdgeEffectView - if let current = self.bottomEdgeEffectView { - bottomEdgeEffectView = current - } else { - bottomEdgeEffectView = EdgeEffectView() - self.view.addSubview(bottomEdgeEffectView) - self.bottomEdgeEffectView = bottomEdgeEffectView + if self.hasBottomEdgeEffect { + let bottomEdgeEffectView: EdgeEffectView + if let current = self.bottomEdgeEffectView { + bottomEdgeEffectView = current + } else { + bottomEdgeEffectView = EdgeEffectView() + self.view.addSubview(bottomEdgeEffectView) + self.bottomEdgeEffectView = bottomEdgeEffectView + } + + let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight)) + transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame) + transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0) + bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) + } else if let bottomEdgeEffectView = self.bottomEdgeEffectView { + bottomEdgeEffectView.removeFromSuperview() } - - let bottomEdgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - edgeEffectHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: edgeEffectHeight)) - transition.updateFrame(view: bottomEdgeEffectView, frame: bottomEdgeEffectFrame) - transition.updateAlpha(layer: bottomEdgeEffectView.layer, alpha: self.isSearching ? 0.0 : 1.0) - bottomEdgeEffectView.update(content: .clear, blur: true, alpha: 1.0, rect: bottomEdgeEffectFrame, edge: .bottom, edgeSize: bottomEdgeEffectFrame.height, transition: ComponentTransition(transition)) } } private struct AttachmentFileControllerState: Equatable { var searching: Bool + var savedMusicExpanded: Bool } -public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { +public enum AttachmentFileControllerMode { + case recent + case audio +} + +public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: AttachmentFileControllerMode = .recent, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController { let actionsDisposable = DisposableSet() - let statePromise = ValuePromise(AttachmentFileControllerState(searching: false), ignoreRepeated: true) - let stateValue = Atomic(value: AttachmentFileControllerState(searching: false)) + let statePromise = ValuePromise(AttachmentFileControllerState(searching: false, savedMusicExpanded: false), ignoreRepeated: true) + let stateValue = Atomic(value: AttachmentFileControllerState(searching: false, savedMusicExpanded: false)) let updateState: ((AttachmentFileControllerState) -> AttachmentFileControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } @@ -265,39 +360,87 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre var updateIsSearchingImpl: ((Bool) -> Void)? let arguments = AttachmentFileControllerArguments( context: context, + isAudio: mode == .audio, openGallery: { presentGallery() }, openFiles: { presentFiles() }, + expandSavedMusic: { + updateState { state in + var updatedState = state + updatedState.savedMusicExpanded = true + return updatedState + } + }, send: { message in - let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true)) - |> `catch` { _ in - return .single(.result([])) - } - |> mapToSignal { result -> Signal<[Message], NoError> in - guard case let .result(result) = result else { - return .complete() + if message.id.namespace == Namespaces.Message.Local { + if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { + send(.standalone(media: file)) } - return .single(result) - } - |> deliverOnMainQueue).startStandalone(next: { messages in - if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { - send(.message(message: MessageReference(message), media: file)) + } else { + let _ = (context.engine.messages.getMessagesLoadIfNecessary([message.id], strategy: .cloud(skipLocal: true)) + |> `catch` { _ in + return .single(.result([])) } - dismissImpl?() - }) + |> mapToSignal { result -> Signal<[Message], NoError> in + guard case let .result(result) = result else { + return .complete() + } + return .single(result) + } + |> deliverOnMainQueue).startStandalone(next: { messages in + if let message = messages.first, let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile { + send(.message(message: MessageReference(message), media: file)) + } + dismissImpl?() + }) + } } ) - let recentDocuments: Signal<[Message]?, NoError> = .single(nil) - |> then( - context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil) - |> map { result -> [Message]? in - return result.0.messages - } - ) + let recentDocuments: Signal<[Message]?, NoError> + let savedMusicContext: ProfileSavedMusicContext? + let savedMusic: Signal<[Message]?, NoError> + switch mode { + case .recent: + recentDocuments = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: "", state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + savedMusicContext = nil + savedMusic = .single(nil) + case .audio: + recentDocuments = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: "", state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + savedMusicContext = ProfileSavedMusicContext(account: context.account, peerId: context.account.peerId) + savedMusic = .single(nil) + |> then( + savedMusicContext!.state + |> map { state in + let peerId = context.account.peerId + var messages: [Message] = [] + let peers = SimpleDictionary() +// if let peer { +// peers[peerId] = peer._asPeer() +// } + for file in state.files { + let stableId = UInt32(clamping: file.fileId.id % Int64(Int32.max)) + messages.append(Message(stableId: stableId, stableVersion: 0, id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: Int32(stableId)), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [.music], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])) + } + return messages + } + ) + } let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData @@ -306,14 +449,12 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre let previousRecentDocuments = Atomic<[Message]?>(value: nil) let signal = combineLatest(queue: Queue.mainQueue(), - presentationData, - recentDocuments, - statePromise.get() - ) - |> map { presentationData, recentDocuments, - state -> (ItemListControllerState, (ItemListNodeState, Any)) in + savedMusic, + statePromise.get() + ) + |> map { presentationData, recentDocuments, savedMusic, state -> (ItemListControllerState, (ItemListNodeState, Any)) in var presentationData = presentationData let updatedTheme = presentationData.theme.withModalBlocksBackground() @@ -397,9 +538,17 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre rightNavigationButton = searchButtonNode.flatMap { ItemListNavigationButton(content: .node($0), style: .regular, enabled: true, action: {}) } } + let title: String + switch mode { + case .recent: + title = presentationData.strings.Attachment_File + case .audio: + title = "Audio" + } + let controllerState = ItemListControllerState( presentationData: ItemListPresentationData(presentationData), - title: .text(presentationData.strings.Attachment_File), + title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), @@ -424,7 +573,7 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre var searchItem: ItemListControllerSearch? if state.searching { - searchItem = AttachmentFileSearchItem(context: context, presentationData: presentationData, focus: { + searchItem = AttachmentFileSearchItem(context: context, mode: mode, presentationData: presentationData, focus: { expandImpl?() }, cancel: { updateState { state in @@ -441,14 +590,18 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre }) } - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, mode: mode, state: state, savedMusic: savedMusic, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() + let _ = savedMusicContext?.state } let controller = AttachmentFileControllerImpl(context: context, state: signal, hideNavigationBarBackground: true) + if case .audio = mode { + controller.hasBottomEdgeEffect = false + } controller.delayDisappear = true controller.visibleBottomContentOffsetChanged = { [weak controller] offset in switch offset { @@ -490,3 +643,34 @@ public func makeAttachmentFileControllerImpl(context: AccountContext, updatedPre } return controller } + +public func storyAudioPickerController( + context: AccountContext, + selectFromFiles: @escaping () -> Void, + dismissed: @escaping () -> Void, + completion: @escaping (AnyMediaReference) -> Void, +) -> ViewController { + var dismissImpl: (() -> Void)? + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) + let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) + let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, style: .glass, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false) + controller.requestController = { _, present in + let filePickerController = makeAttachmentFileControllerImpl(context: context, updatedPresentationData: updatedPresentationData, mode: .audio, bannedSendMedia: nil, presentGallery: {}, presentFiles: { + selectFromFiles() + dismissImpl?() + }, send: { file in + completion(file) + dismissImpl?() + }) as! AttachmentFileControllerImpl + present(filePickerController, filePickerController.mediaPickerContext) + } + controller.navigationPresentation = .flatModal + controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + controller.didDismiss = { + dismissed() + } + dismissImpl = { [weak controller] in + controller?.dismiss(animated: true) + } + return controller +} diff --git a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift index 3eaed92670..ed7b71bd06 100644 --- a/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift +++ b/submodules/TelegramUI/Components/AttachmentFileController/Sources/AttachmentFileSearchItem.swift @@ -21,6 +21,7 @@ import SearchInputPanelComponent final class AttachmentFileSearchItem: ItemListControllerSearch { let context: AccountContext + let mode: AttachmentFileControllerMode let presentationData: PresentationData let focus: () -> Void let cancel: () -> Void @@ -31,8 +32,9 @@ final class AttachmentFileSearchItem: ItemListControllerSearch { private var activity: ValuePromise = ValuePromise(ignoreRepeated: false) private let activityDisposable = MetaDisposable() - init(context: AccountContext, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) { + init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) { self.context = context + self.mode = mode self.presentationData = presentationData self.focus = focus self.cancel = cancel @@ -69,7 +71,7 @@ final class AttachmentFileSearchItem: ItemListControllerSearch { } func node(current: ItemListControllerSearchNode?, titleContentNode: (NavigationBarContentNode & ItemListControllerSearchNavigationContentNode)?) -> ItemListControllerSearchNode { - return AttachmentFileSearchItemNode(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in + return AttachmentFileSearchItemNode(context: self.context, mode: self.mode, presentationData: self.presentationData, focus: self.focus, send: self.send, cancel: self.cancel, updateActivity: { [weak self] value in self?.activity.set(value) }, dismissInput: self.dismissInput) } @@ -77,8 +79,8 @@ final class AttachmentFileSearchItem: ItemListControllerSearch { private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { private let context: AccountContext - private let theme: PresentationTheme - private let strings: PresentationStrings + private let mode: AttachmentFileControllerMode + private let presentationData: PresentationData private let focus: () -> Void private let cancel: () -> Void @@ -88,14 +90,14 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { private var validLayout: ContainerViewLayout? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) { + init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, focus: @escaping () -> Void, send: @escaping (Message) -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(Bool) -> Void, dismissInput: @escaping () -> Void) { self.context = context - self.theme = theme - self.strings = strings + self.mode = mode + self.presentationData = presentationData self.focus = focus self.cancel = cancel - self.containerNode = AttachmentFileSearchContainerNode(context: context, forceTheme: nil, send: { message in + self.containerNode = AttachmentFileSearchContainerNode(context: context, mode: mode, presentationData: presentationData, send: { message in send(message) }, updateActivity: updateActivity) @@ -136,15 +138,16 @@ private final class AttachmentFileSearchItemNode: ItemListControllerSearchNode { transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight))) self.containerNode.containerLayoutUpdated(layout.withUpdatedSize(CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight)), navigationBarHeight: 0.0, transition: transition) + //TODO:localize let searchInputSize = self.searchInput.update( transition: .immediate, component: AnyComponent( SearchInputPanelComponent( - theme: self.theme, - strings: self.strings, + theme: self.presentationData.theme, + strings: self.presentationData.strings, metrics: layout.metrics, safeInsets: layout.safeInsets, - placeholder: self.strings.Attachment_FilesSearchPlaceholder, + placeholder: self.mode == .audio ? "Search shared audio" : self.presentationData.strings.Attachment_FilesSearchPlaceholder, updated: { [weak self] query in guard let self else { return @@ -251,7 +254,7 @@ private final class AttachmentFileSearchEntry: Comparable, Identifiable { interaction.send(message) return false }, openMessageContextMenu: { _, _, _, _, _ in }, toggleMessagesSelection: { _, _ in }, openUrl: { _, _, _, _ in }, openInstantPage: { _, _ in }, longTap: { _, _ in }, getHiddenMedia: { return [:] }) - return ListMessageItem(presentationData: ChatPresentationData(presentationData: interaction.context.sharedContext.currentPresentationData.with({$0})), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain) + return ListMessageItem(presentationData: ChatPresentationData(presentationData: presentationData), systemStyle: .glass, context: interaction.context, chatLocation: .peer(id: PeerId(0)), interaction: itemInteraction, message: message, selection: .none, displayHeader: true, displayFileInfo: false, displayBackground: true, style: .plain) } } @@ -293,10 +296,8 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon private let emptyQueryDisposable = MetaDisposable() private let searchDisposable = MetaDisposable() - private let forceTheme: PresentationTheme? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - private let presentationDataPromise: Promise private var _hasDim: Bool = false @@ -304,17 +305,12 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon return _hasDim } - public init(context: AccountContext, forceTheme: PresentationTheme?, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) { + public init(context: AccountContext, mode: AttachmentFileControllerMode, presentationData: PresentationData, send: @escaping (Message) -> Void, updateActivity: @escaping (Bool) -> Void) { self.context = context self.send = send - let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData - self.forceTheme = forceTheme - if let forceTheme = self.forceTheme { - self.presentationData = self.presentationData.withUpdated(theme: forceTheme) - } self.presentationDataPromise = Promise(self.presentationData) self.dimNode = ASDisplayNode() @@ -380,13 +376,26 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon return .single(nil) } - let signal: Signal<[Message]?, NoError> = .single(nil) - |> then( - context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil) - |> map { result -> [Message]? in - return result.0.messages - } - ) + let signal: Signal<[Message]?, NoError> + switch mode { + case .recent: + signal = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .sentMedia(tags: [.file]), query: query, state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + case .audio: + signal = .single(nil) + |> then( + context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: [.music], minDate: nil, maxDate: nil), query: query, state: nil) + |> map { result -> [Message]? in + return result.0.messages + } + ) + } + updateActivity(true) return combineLatest(signal, presentationDataPromise.get()) @@ -420,25 +429,19 @@ public final class AttachmentFileSearchContainerNode: SearchDisplayControllerCon } })) - self.presentationDataDisposable = (context.sharedContext.presentationData - |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in - if let strongSelf = self { - var presentationData = presentationData - - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings - - if let forceTheme = strongSelf.forceTheme { - presentationData = presentationData.withUpdated(theme: forceTheme) - } - - strongSelf.presentationData = presentationData - - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) - } - } - }) +// self.presentationDataDisposable = (context.sharedContext.presentationData +// |> deliverOnMainQueue).startStrict(next: { [weak self] presentationData in +// if let strongSelf = self { +// let previousTheme = strongSelf.presentationData.theme +// let previousStrings = strongSelf.presentationData.strings +// +// strongSelf.presentationData = presentationData +// +// if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { +// strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) +// } +// } +// }) self.listNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD index 7e0d886f68..cfc119906d 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/BUILD @@ -80,6 +80,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageUnsupportedBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode", "//submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index bcbf47663a..f15cdb59df 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -70,6 +70,7 @@ import ChatMessageStoryMentionContentNode import ChatMessageUnsupportedBubbleContentNode import ChatMessageWallpaperBubbleContentNode import ChatMessageGiftBubbleContentNode +import ChatMessageGiftOfferBubbleContentNode import ChatMessageGiveawayBubbleContentNode import ChatMessageJoinedChannelBubbleContentNode import ChatMessageFactCheckBubbleContentNode @@ -246,6 +247,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else if case .suggestedBirthday = action.action { result.append((message, ChatMessageBirthdateSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) + } else if case .starGiftPurchaseOffer = action.action { + result.append((message, ChatMessageGiftOfferBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } else { if !canAddMessageReactions(message: message) { needReactions = false @@ -2821,6 +2824,48 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI actionButtonsFinalize = buttonsLayout lastNodeTopPosition = .None(.Both) + } else if incoming, let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(_, _, expireDate, isAccepted, isDeclined) = action.action, !isAccepted && !isDeclined { + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if expireDate <= currentTimestamp { + + } else { + var buttonDeclineValue: UInt8 = 0 + let buttonDecline = MemoryBuffer(data: Data(bytes: &buttonDeclineValue, count: 1)) + var buttonApproveValue: UInt8 = 1 + let buttonApprove = MemoryBuffer(data: Data(bytes: &buttonApproveValue, count: 1)) + + let customInfos: [MemoryBuffer: ChatMessageActionButtonsNode.CustomInfo] = [ + buttonDecline: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostReject + ), + buttonApprove: ChatMessageActionButtonsNode.CustomInfo( + isEnabled: true, + icon: .suggestedPostApprove + ) + ] + //TODO:localize + let (minWidth, buttonsLayout) = actionButtonsLayout( + item.context, + item.presentationData.theme, + item.presentationData.chatBubbleCorners, + item.presentationData.strings, + item.controllerInteraction.presentationContext.backgroundNode, + ReplyMarkupMessageAttribute( + rows: [ + ReplyMarkupRow(buttons: [ + ReplyMarkupButton(title: "Reject", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonDecline)), + ReplyMarkupButton(title: "Accept", titleWhenForwarded: nil, action: .callback(requiresPassword: false, data: buttonApprove)) + ]) + ], + flags: [], + placeholder: nil + ), customInfos, item.message, baseWidth) + maxContentWidth = max(maxContentWidth, minWidth) + actionButtonsFinalize = buttonsLayout + + lastNodeTopPosition = .None(.Both) + } } else if incoming, let attribute = item.message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute, attribute.state == nil { var canApprove = true if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, peer.isMonoForum, let linkedMonoforumId = peer.linkedMonoforumId, let mainChannel = item.message.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect), !mainChannel.hasPermission(.sendSomething) { @@ -4772,7 +4817,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if let actionButtonsSizeAndApply = actionButtonsSizeAndApply { let actionButtonsNode = actionButtonsSizeAndApply.1(animation) - let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0) + var actionButtonsOriginX = backgroundFrame.minX + if case .center = alignment { + actionButtonsOriginX += 3.0 + } else { + actionButtonsOriginX += incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right + } + let actionButtonsFrame = CGRect(origin: CGPoint(x: actionButtonsOriginX, y: backgroundFrame.maxY), size: actionButtonsSizeAndApply.0) if actionButtonsNode !== strongSelf.actionButtonsNode { strongSelf.actionButtonsNode = actionButtonsNode actionButtonsNode.buttonPressed = { [weak strongSelf] button, progress in diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index b65bd9c04a..df5f8f86aa 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -430,6 +430,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { var buttonTitle = item.presentationData.strings.Notification_PremiumGift_View var buttonIcon: String? var ribbonTitle = "" + var customRibbonColors: [UIColor]? var textSpacing: CGFloat = 0.0 var isStarGift = false @@ -686,7 +687,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } } } - case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, _): + case let .starGiftUnique(gift, isUpgrade, _, _, _, _, isRefunded, _, _, _, _, _, _, _, _, _, fromOffer): if case let .unique(uniqueGift) = gift { isStarGift = true @@ -715,7 +716,13 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { title = item.presentationData.strings.Notification_StarGift_Title(authorName).string } text = isStoryEntity ? "**\(item.presentationData.strings.Notification_StarGift_Collectible) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" : "**\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: item.presentationData.dateTimeFormat))**" - ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift + if fromOffer { + //TODO:localize + ribbonTitle = incoming ? "" : "sold" + customRibbonColors = [UIColor(rgb: 0xd9433a), UIColor(rgb: 0xff645b)] + } else { + ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift + } buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View modelTitle = item.presentationData.strings.Notification_StarGift_Model backdropTitle = item.presentationData.strings.Notification_StarGift_Backdrop @@ -1285,7 +1292,9 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { if ribbonTextLayout.size.width > 0.0 { if strongSelf.ribbonBackgroundNode.image == nil { - if let uniqueBackgroundColor { + if let customRibbonColors { + strongSelf.ribbonBackgroundNode.image = generateGradientTintedImage(image: UIImage(bundleImageName: "Premium/GiftRibbon"), colors: customRibbonColors, direction: .mirroredDiagonal) + } else if let uniqueBackgroundColor { let colors = [ uniqueBackgroundColor.withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89), uniqueBackgroundColor.withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD new file mode 100644 index 0000000000..db4d3ca487 --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/BUILD @@ -0,0 +1,36 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatMessageGiftOfferBubbleContentNode", + module_name = "ChatMessageGiftOfferBubbleContentNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramCore", + "//submodules/AccountContext", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUIPreferences", + "//submodules/TextFormat", + "//submodules/LocalizedPeerData", + "//submodules/UrlEscaping", + "//submodules/TelegramStringFormatting", + "//submodules/WallpaperBackgroundNode", + "//submodules/ReactionSelectionNode", + "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/Markdown", + "//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode", + "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/TelegramUI/Components/Gifts/GiftItemComponent", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift new file mode 100644 index 0000000000..2372c3183d --- /dev/null +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftOfferBubbleContentNode/Sources/ChatMessageGiftOfferBubbleContentNode.swift @@ -0,0 +1,341 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import SwiftSignalKit +import ComponentFlow +import TelegramCore +import AccountContext +import TelegramPresentationData +import TelegramUIPreferences +import TextFormat +import LocalizedPeerData +import UrlEscaping +import TelegramStringFormatting +import WallpaperBackgroundNode +import ReactionSelectionNode +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import ChatControllerInteraction +import ShimmerEffect +import Markdown +import ChatMessageBubbleContentNode +import ChatMessageItemCommon +import TextNodeWithEntities +import GiftItemComponent + +private func attributedServiceMessageString(theme: ChatPresentationThemeData, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, dateTimeFormat: PresentationDateTimeFormat, message: EngineMessage, accountPeerId: EnginePeer.Id) -> NSAttributedString? { + return universalServiceMessageString(presentationData: (theme.theme, theme.wallpaper), strings: strings, nameDisplayOrder: nameDisplayOrder, dateTimeFormat: dateTimeFormat, message: message, accountPeerId: accountPeerId, forChatList: false, forForumOverview: false, forAdditionalServiceMessage: true) +} + +public class ChatMessageGiftOfferBubbleContentNode: ChatMessageBubbleContentNode { + private var mediaBackgroundContent: WallpaperBubbleBackgroundNode? + private let titleNode: TextNode + private let subtitleNode: TextNodeWithEntities + private let giftIcon = ComponentView() + + private var absoluteRect: (CGRect, CGSize)? + + private var isPlaying: Bool = false + + override public var disablesClipping: Bool { + return true + } + + override public var visibility: ListViewItemNodeVisibility { + didSet { + let wasVisible = oldValue != .none + let isVisible = self.visibility != .none + + if wasVisible != isVisible { + self.visibilityStatus = isVisible + + switch self.visibility { + case .none: + self.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + self.subtitleNode.visibilityRect = subRect + } + } + } + } + + private var visibilityStatus: Bool? { + didSet { + if self.visibilityStatus != oldValue { + self.updateVisibility() + } + } + } + + private var fetchDisposable: Disposable? + private var setupTimestamp: Double? + + private var cachedTonImage: (UIImage, UIColor)? + + required public init() { + self.titleNode = TextNode() + self.titleNode.isUserInteractionEnabled = false + self.titleNode.displaysAsynchronously = false + + self.subtitleNode = TextNodeWithEntities() + self.subtitleNode.textNode.isUserInteractionEnabled = false + self.subtitleNode.textNode.displaysAsynchronously = false + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.subtitleNode.textNode) + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.fetchDisposable?.dispose() + } + + override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { + let makeTitleLayout = TextNode.asyncLayout(self.titleNode) + let makeSubtitleLayout = TextNodeWithEntities.asyncLayout(self.subtitleNode) + + return { item, layoutConstants, _, _, _, _ in + let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center) + + return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in + var giftSize = CGSize(width: 260.0, height: 240.0) + var uniqueGift: StarGift.UniqueGift? + + let incoming: Bool + if item.message.id.peerId == item.context.account.peerId && item.message.forwardInfo == nil { + incoming = true + } else { + incoming = item.message.effectivelyIncoming(item.context.account.peerId) + } + + let textColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText + + let text: String + let additionalText: String + + var hasActionButtons = false + if let action = item.message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, expireDate, isAccepted, isDeclined) = action.action { + let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + let priceString: String + switch amount.currency { + case .stars: + priceString = "\(amount.amount) Stars" + case .ton: + priceString = "\(amount.amount) TON" + } + + let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0) }?.compactDisplayTitle ?? "" + let giftTitle: String + if case let .unique(gift) = gift { + giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: item.presentationData.dateTimeFormat))" + uniqueGift = gift + } else { + giftTitle = "" + } + + //TODO:localize + if incoming { + text = "**\(peerName)** offered you **\(priceString)** for your gift **\(giftTitle)**." + } else { + text = "You offered **\(peerName)** **\(priceString)** for their gift **\(giftTitle)**." + } + + if isAccepted { + additionalText = "This offer was accepted." + } else if isDeclined { + additionalText = "This offer was rejected." + } else if expireDate > currentTimestamp { + func textForTimeout(_ value: Int32) -> String { + if value < 3600 { + let minutes = value / 60 + //let seconds = value % 60 + //let secondsPadding = seconds < 10 ? "0" : "" + return "\(minutes)m" //\(secondsPadding)\(seconds)" + } else { + let hours = value / 3600 + let minutes = (value % 3600) / 60 + let minutesPadding = minutes < 10 ? "0" : "" + //let seconds = value % 60 + //let secondsPadding = seconds < 10 ? "0" : "" + return "\(hours)h \(minutesPadding)\(minutes)m" //:\(secondsPadding)\(seconds)" + } + } + let delta = expireDate - currentTimestamp + additionalText = "This offer expires in \(textForTimeout(delta))." + + if incoming { + hasActionButtons = true + } + } else { + additionalText = "This offer has expired." + } + } else { + text = "" + additionalText = "" + } + + let titleAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: additionalText, font: Font.regular(13.0), textColor: textColor, paragraphAlignment: .center)) + + let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: textColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: textColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + + let textConstrainedSize = CGSize(width: giftSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude) + let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets())) + + giftSize.height = titleLayout.size.height + subtitleLayout.size.height + 162.0 + + let backgroundSize = CGSize(width: giftSize.width, height: giftSize.height + 4.0) + + return (backgroundSize.width, { boundingWidth in + return (backgroundSize, { [weak self] animation, synchronousLoads, info in + if let strongSelf = self { + strongSelf.item = item + + let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((boundingWidth - giftSize.width) / 2.0), y: 0.0), size: giftSize) + let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0) + + strongSelf.updateVisibility() + + let _ = titleApply() + let _ = subtitleApply(TextNodeWithEntities.Arguments( + context: item.context, + cache: item.controllerInteraction.presentationContext.animationCache, + renderer: item.controllerInteraction.presentationContext.animationRenderer, + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads + )) + + + let textFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0), y: mediaBackgroundFrame.minY + 126.0), size: subtitleLayout.size) + strongSelf.subtitleNode.textNode.frame = textFrame + + let titleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - titleLayout.size.width) / 2.0) , y: textFrame.maxY + 23.0), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame + + if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) { + backgroundContent.clipsToBounds = true + backgroundContent.cornerRadius = 24.0 + + strongSelf.mediaBackgroundContent = backgroundContent + strongSelf.insertSubnode(backgroundContent, at: 0) + } + + if let backgroundContent = strongSelf.mediaBackgroundContent { + animation.animator.updateFrame(layer: backgroundContent.layer, frame: mediaBackgroundFrame, completion: nil) + backgroundContent.clipsToBounds = true + + if hasActionButtons { + backgroundContent.cornerRadius = 0.0 + if backgroundContent.view.mask == nil { + backgroundContent.view.mask = UIImageView(image: generateImage(mediaBackgroundFrame.size, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + + context.addPath(CGPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height * 0.5), cornerWidth: 24.0, cornerHeight: 24.0, transform: nil)) + context.addPath(CGPath(roundedRect: CGRect(x: 0, y: size.height * 0.5 - 30.0, width: size.width, height: size.height * 0.5 + 30.0), cornerWidth: 8.0, cornerHeight: 8.0, transform: nil)) + context.fillPath() + })) + } + } else { + backgroundContent.view.mask = nil + backgroundContent.cornerRadius = 24.0 + } + } + + if let uniqueGift { + let iconSize = CGSize(width: 94.0, height: 94.0) + let _ = strongSelf.giftIcon.update( + transition: .immediate, + component: AnyComponent(GiftItemComponent( + context: item.context, + theme: item.presentationData.theme.theme, + strings: item.presentationData.strings, + peer: nil, + subject: .uniqueGift(gift: uniqueGift, price: nil), + mode: .thumbnail + )), + environment: {}, + containerSize: iconSize + ) + if let giftIconView = strongSelf.giftIcon.view { + if giftIconView.superview == nil { + // backgroundView.layer.cornerRadius = 20.0 + //backgroundView.clipsToBounds = true + strongSelf.view.addSubview(giftIconView) + } + giftIconView.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - iconSize.width) / 2.0), y: mediaBackgroundFrame.minY + 17.0), size: iconSize) + } + } + + if let (rect, size) = strongSelf.absoluteRect { + strongSelf.updateAbsoluteRect(rect, within: size) + } + + switch strongSelf.visibility { + case .none: + strongSelf.subtitleNode.visibilityRect = nil + case let .visible(_, subRect): + var subRect = subRect + subRect.origin.x = 0.0 + subRect.size.width = 10000.0 + strongSelf.subtitleNode.visibilityRect = subRect + } + } + }) + }) + }) + } + } + + override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.absoluteRect = (rect, containerSize) + + if let mediaBackgroundContent = self.mediaBackgroundContent { + var backgroundFrame = mediaBackgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate) + } + } + + override public func applyAbsoluteOffset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) { + + } + + override public func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { + + } + + override public func unreadMessageRangeUpdated() { + self.updateVisibility() + } + + override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.mediaBackgroundContent?.frame.contains(point) == true { + return ChatMessageBubbleContentTapAction(content: .openMessage) + } else { + return ChatMessageBubbleContentTapAction(content: .none) + } + } + + private func updateVisibility() { + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD index cf18fe2633..555ad4a0a5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/Chat/ChatMessageDateAndStatusNode", "//submodules/TelegramUI/Components/Chat/ChatHistoryEntry", "//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon", + "//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode", "//submodules/AnimatedCountLabelNode", "//submodules/AudioWaveform", "//submodules/DeviceProximity", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift index 60857c7020..550414b478 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveFileNode/Sources/ChatMessageInteractiveFileNode.swift @@ -35,6 +35,7 @@ import TelegramStringFormatting import AnimatedCountLabelNode import AudioWaveform import DeviceProximity +import ShimmeringLinkNode private struct FetchControls { let fetch: (Bool) -> Void @@ -123,6 +124,7 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { public let fetchingTextNode: ImmediateTextNode public let fetchingCompactTextNode: ImmediateTextNode private let countNode: ImmediateAnimatedCountLabelNode + private var shimmeringNodes: [ShimmeringLinkNode] = [] public var waveformView: ComponentHostView? @@ -809,23 +811,28 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { var displayTrailingAnimatedDots = false + var isTranslating = false if let transcribedText = transcribedText, case .expanded = effectiveAudioTranscriptionState { switch transcribedText { case let .success(text, isPending): textString = NSAttributedString(string: text, font: textFont, textColor: messageTheme.primaryTextColor) - /*#if DEBUG - var isPending = isPending - if "".isEmpty { - isPending = true - } - #endif*/ - if isPending { let modifiedString = NSMutableAttributedString(attributedString: textString!) modifiedString.append(NSAttributedString(string: "...", font: textFont, textColor: .clear)) displayTrailingAnimatedDots = true textString = modifiedString + } else { + if let translateToLanguage = arguments.associatedData.translateToLanguage, !text.isEmpty && arguments.incoming { + isTranslating = true + for attribute in arguments.message.attributes { + if let attribute = attribute as? TranslationMessageAttribute, !attribute.text.isEmpty, attribute.toLang == translateToLanguage { + textString = NSAttributedString(string: attribute.text, font: textFont, textColor: messageTheme.primaryTextColor) + isTranslating = false + break + } + } + } } case let .error(error): let errorTextFont = Font.regular(floor(arguments.presentationData.fontSize.baseDisplaySize * 15.0 / 17.0)) @@ -1538,6 +1545,8 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } else { strongSelf.dateAndStatusNode.pressed = nil } + + strongSelf.updateIsTranslating(isTranslating) } }) }) @@ -1545,6 +1554,39 @@ public final class ChatMessageInteractiveFileNode: ASDisplayNode { } } + private func updateIsTranslating(_ isTranslating: Bool) { + guard let arguments = self.arguments else { + return + } + var rects: [[CGRect]] = [] + let titleRects = (self.textNode.rangeRects(in: NSRange(location: 0, length: self.textNode.cachedLayout?.attributedString?.length ?? 0))?.rects ?? []).map { self.textNode.view.convert($0, to: self.textClippingNode.view) } + rects.append(titleRects) + + if isTranslating, !rects.isEmpty { + if self.shimmeringNodes.isEmpty { + for rects in rects { + let shimmeringNode = ShimmeringLinkNode(color: arguments.message.effectivelyIncoming(arguments.context.account.peerId) ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor.withAlphaComponent(0.1) : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor.withAlphaComponent(0.1)) + shimmeringNode.updateRects(rects) + shimmeringNode.frame = self.bounds + shimmeringNode.updateLayout(self.bounds.size) + shimmeringNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.shimmeringNodes.append(shimmeringNode) + self.textClippingNode.insertSubnode(shimmeringNode, belowSubnode: self.textNode) + } + } + } else if !self.shimmeringNodes.isEmpty { + let shimmeringNodes = self.shimmeringNodes + self.shimmeringNodes = [] + + for shimmeringNode in shimmeringNodes { + shimmeringNode.alpha = 0.0 + shimmeringNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak shimmeringNode] _ in + shimmeringNode?.removeFromSupernode() + }) + } + } + } + private func updateStatus(animated: Bool) { guard let resourceStatus = self.resourceStatus else { return diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD index 1d1499967b..393edc5f09 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/BUILD @@ -45,6 +45,9 @@ swift_library( "//submodules/AnimatedCountLabelNode", "//submodules/HexColor", "//submodules/QrCodeUI", + "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/Components/BundleIconComponent", + "//submodules/Components/SheetComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift index 6b69db21d9..8ada9fca44 100644 --- a/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift +++ b/submodules/TelegramUI/Components/Chat/ChatQrCodeScreen/Sources/ChatQrCodeScreen.swift @@ -36,6 +36,10 @@ import SegmentedControlNode import AnimatedCountLabelNode import HexColor import QrCodeUI +import ComponentFlow +import GlassBarButtonComponent +import SheetComponent +import BundleIconComponent private func closeButtonImage(theme: PresentationTheme) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in @@ -717,7 +721,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen { } private func iconColors(theme: PresentationTheme) -> [String: UIColor] { - let accentColor = theme.actionSheet.controlAccentColor + let accentColor = theme.rootController.navigationBar.glassBarButtonForegroundColor var colors: [String: UIColor] = [:] colors["Sunny.Path 14.Path.Stroke 1"] = accentColor colors["Sunny.Path 15.Path.Stroke 1"] = accentColor @@ -745,18 +749,6 @@ private func interpolateColors(from: [String: UIColor], to: [String: UIColor], f private let defaultEmoticon = "🏠" -private func generateShadowImage() -> UIImage? { - return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setShadow(offset: CGSize(width: 0.0, height: -0.5), blur: 10.0, color: UIColor(rgb: 0x000000, alpha: 0.4).cgColor) - context.setFillColor(UIColor(rgb: 0x000000, alpha: 0.4).cgColor) - let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: 40.0, height: 40.0)), cornerRadius: 16.0) - context.addPath(path.cgPath) - context.fillPath() - })?.stretchableImage(withLeftCapWidth: 20, topCapHeight: 0) -} - private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData @@ -768,14 +760,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg private let scrollNodeContentNode: ASDisplayNode private let contentContainerNode: ASDisplayNode private let topContentContainerNode: SparseNode - private let shadowNode: ASImageNode - private let effectNode: ASDisplayNode private let backgroundNode: ASDisplayNode - private let contentBackgroundNode: ASDisplayNode + private let contentBackgroundView: SheetBackgroundView + private let cancelButton = ComponentView() + private let switchThemeButton = ComponentView() private let titleNode: ASTextNode private let segmentedNode: SegmentedControlNode - private let cancelButton: HighlightableButtonNode - private let switchThemeButton: HighlightTrackingButtonNode + private let cancelButtonNode: HighlightableButtonNode + private let switchThemeButtonNode: HighlightTrackingButtonNode private let animationContainerNode: ASDisplayNode private var animationNode: AnimationNode private let doneButton: SolidRoundedButtonNode @@ -841,28 +833,17 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.topContentContainerNode = SparseNode() self.topContentContainerNode.isOpaque = false - - self.shadowNode = ASImageNode() - self.shadowNode.contentMode = .scaleToFill - self.shadowNode.image = generateShadowImage() self.backgroundNode = ASDisplayNode() self.backgroundNode.clipsToBounds = true - self.backgroundNode.cornerRadius = 16.0 + self.backgroundNode.cornerRadius = 38.0 self.isDarkAppearance = self.presentationData.theme.overallDarkAppearance self.isDarkAppearancePromise = ValuePromise(self.presentationData.theme.overallDarkAppearance) - let backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor let textColor = self.presentationData.theme.actionSheet.primaryTextColor - let blurStyle: UIBlurEffect.Style = self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark - - self.effectNode = ASDisplayNode(viewBlock: { - return UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) - }) - - self.contentBackgroundNode = ASDisplayNode() - self.contentBackgroundNode.backgroundColor = backgroundColor + + self.contentBackgroundView = SheetBackgroundView() self.titleNode = ASTextNode() let title: String @@ -878,12 +859,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.segmentedNode.isHidden = !self.contentNode.hasVideo self.titleNode.isHidden = !self.segmentedNode.isHidden - self.cancelButton = HighlightableButtonNode() - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) - self.cancelButton.accessibilityLabel = self.presentationData.strings.Common_Close - self.cancelButton.accessibilityTraits = [.button] + self.cancelButtonNode = HighlightableButtonNode() + self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + self.cancelButtonNode.accessibilityLabel = self.presentationData.strings.Common_Close + self.cancelButtonNode.accessibilityTraits = [.button] - self.switchThemeButton = HighlightTrackingButtonNode() + self.switchThemeButtonNode = HighlightTrackingButtonNode() self.animationContainerNode = ASDisplayNode() self.animationContainerNode.isUserInteractionEnabled = false @@ -916,13 +897,10 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.scrollNodeContentNode.addSubnode(self.contentNode) - self.scrollNodeContentNode.addSubnode(self.shadowNode) self.scrollNodeContentNode.addSubnode(self.backgroundNode) self.scrollNodeContentNode.addSubnode(self.contentContainerNode) self.scrollNodeContentNode.addSubnode(self.topContentContainerNode) - self.backgroundNode.addSubnode(self.effectNode) - self.backgroundNode.addSubnode(self.contentBackgroundNode) self.contentContainerNode.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.segmentedNode) self.contentContainerNode.addSubnode(self.doneButton) @@ -930,12 +908,12 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.topContentContainerNode.addSubnode(self.animationContainerNode) self.animationContainerNode.addSubnode(self.animationNode) - self.topContentContainerNode.addSubnode(self.switchThemeButton) + //self.topContentContainerNode.addSubnode(self.switchThemeButtonNode) self.topContentContainerNode.addSubnode(self.listNode) - self.topContentContainerNode.addSubnode(self.cancelButton) + //self.topContentContainerNode.addSubnode(self.cancelButtonNode) - self.switchThemeButton.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) - self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) + self.switchThemeButtonNode.addTarget(self, action: #selector(self.switchThemePressed), forControlEvents: .touchUpInside) + self.cancelButtonNode.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) self.segmentedNode.selectedIndexChanged = { [weak self] index in guard let strongSelf = self, let contentNode = strongSelf.contentNode as? MessageContentNode, let videoNode = contentNode.videoNode else { @@ -1181,7 +1159,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg } })) - self.switchThemeButton.highligthedChanged = { [weak self] highlighted in + self.switchThemeButtonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.animationContainerNode.layer.removeAnimation(forKey: "opacity") @@ -1280,14 +1258,14 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } - self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) + self.cancelButtonNode.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) self.scanButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: .clear, foregroundColor: self.presentationData.theme.actionSheet.controlAccentColor)) let previousIconColors = iconColors(theme: previousTheme) let newIconColors = iconColors(theme: self.presentationData.theme) - if !self.switchThemeButton.isUserInteractionEnabled { + if !self.switchThemeButtonNode.isUserInteractionEnabled { let themeCrossfadeDuration: Double = 0.3 let themeCrossfadeDelay: Double = 0.25 @@ -1312,6 +1290,8 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never } + self.backgroundNode.view.addSubview(self.contentBackgroundView) + self.listNode.view.disablesInteractiveTransitionGestureRecognizer = true } @@ -1320,9 +1300,9 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg } @objc private func switchThemePressed() { - self.switchThemeButton.isUserInteractionEnabled = false + self.switchThemeButtonNode.isUserInteractionEnabled = false Queue.mainQueue().after(0.5) { - self.switchThemeButton.isUserInteractionEnabled = true + self.switchThemeButtonNode.isUserInteractionEnabled = true } self.animateCrossfade(animateIcon: false) @@ -1372,18 +1352,11 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg }) } - Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) { - if let effectView = self.effectNode.view as? UIVisualEffectView { - UIView.animate(withDuration: ChatQrCodeScreenImpl.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) { - effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } completion: { _ in - } - } - - let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor - self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration) - } +// Queue.mainQueue().after(ChatQrCodeScreenImpl.themeCrossfadeDelay) { +// let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear +// self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor +// self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatQrCodeScreenImpl.themeCrossfadeDuration) +// } if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.contentContainerNode.frame @@ -1403,7 +1376,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg private var animatedOut = false public func animateIn() { - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let offset = self.bounds.size.height - self.contentBackgroundView.frame.minY if let (layout, _) = self.containerLayout { self.scrollNodeContentNode.cornerRadius = layout.deviceMetrics.screenCornerRadius @@ -1424,7 +1397,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg self.wrappingScrollNode.view.isScrollEnabled = false - let distance = self.bounds.size.height - self.contentBackgroundNode.frame.minY + let distance = self.bounds.size.height - self.contentBackgroundView.frame.minY if let velocity { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity / distance) self.wrappingScrollNode.layer.animateSpring(from: 0.0 as NSNumber, to: -distance as NSNumber, keyPath: "bounds.origin.y", duration: 0.45, delay: 0.0, initialVelocity: initialVelocity, damping: 124.0, removeOnCompletion: false, additive: true, completion: { _ in @@ -1473,24 +1446,81 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) let sideInset = floor((layout.size.width - width) / 2.0) - let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) + let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight - 6.0), size: CGSize(width: width, height: contentHeight)).insetBy(dx: 6.0, dy: 0.0) let contentFrame = contentContainerFrame var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: contentFrame.width, height: contentFrame.height + 2000.0)) if backgroundFrame.minY < contentFrame.minY { backgroundFrame.origin.y = contentFrame.minY } - - let shadowFrame = CGRect(x: backgroundFrame.minX, y: backgroundFrame.minY - 8.0, width: backgroundFrame.width, height: 40.0) - transition.updateFrame(node: self.shadowNode, frame: shadowFrame) + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) - transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) - transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + self.contentBackgroundView.update(size: contentFrame.size, color: self.presentationData.theme.actionSheet.opaqueItemBackgroundColor, topCornerRadius: 38.0, bottomCornerRadius: layout.deviceMetrics.screenCornerRadius - 2.0, transition: .immediate) + transition.updateFrame(view: self.contentBackgroundView, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) + + let barButtonSize = CGSize(width: 40.0, height: 40.0) + let cancelButtonSize = self.cancelButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: self.presentationData.theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { [weak self] _ in + self?.cancelButtonPressed() + } + )), + environment: {}, + containerSize: barButtonSize + ) + let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize) + if let view = self.cancelButton.view { + if view.superview == nil { + self.topContentContainerNode.view.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: cancelButtonFrame.size) + view.center = cancelButtonFrame.center + } + + let switchThemeButtonSize = self.switchThemeButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: barButtonSize, + backgroundColor: self.presentationData.theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: self.presentationData.theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "switchTheme", component: AnyComponent( + Rectangle(color: .clear) + )), + action: { [weak self] _ in + self?.switchThemePressed() + } + )), + environment: {}, + containerSize: barButtonSize + ) + let switchThemeButtonFrame = CGRect(origin: CGPoint(x: contentFrame.width - switchThemeButtonSize.width - 16.0, y: 16.0), size: switchThemeButtonSize) + if let view = self.switchThemeButton.view { + if view.superview == nil { + self.topContentContainerNode.view.addSubview(view) + self.topContentContainerNode.view.bringSubviewToFront(self.animationContainerNode.view) + } + view.bounds = CGRect(origin: .zero, size: switchThemeButtonFrame.size) + view.center = switchThemeButtonFrame.center + } + transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.scrollNodeContentNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height + 2000.0))) let titleSize = self.titleNode.measure(CGSize(width: width - 90.0, height: titleHeight)) - let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 19.0 + UIScreenPixel), size: titleSize) + let titleFrame = CGRect(origin: CGPoint(x: floor((contentFrame.width - titleSize.width) / 2.0), y: 36.0 - titleSize.height / 2.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let segmentedSize = self.segmentedNode.updateLayout(.sizeToFit(maximumWidth: width - 90.0, minimumWidth: 160.0, height: 32.0), transition: transition) @@ -1498,20 +1528,20 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg let switchThemeSize = CGSize(width: 44.0, height: 44.0) let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) - transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) - transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.switchThemeButtonNode, frame: switchThemeFrame) + transition.updateFrame(node: self.animationContainerNode, frame: switchThemeButtonFrame.insetBy(dx: 5.0, dy: 5.0)) transition.updateFrameAsPositionAndBounds(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) let cancelSize = CGSize(width: 44.0, height: 44.0) let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) - transition.updateFrame(node: self.cancelButton, frame: cancelFrame) + transition.updateFrame(node: self.cancelButtonNode, frame: cancelFrame) - let buttonInset: CGFloat = 16.0 + let buttonInset: CGFloat = 30.0 let scanButtonHeight = self.scanButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom - 6.0, width: contentFrame.width, height: scanButtonHeight)) + transition.updateFrame(node: self.scanButton, frame: CGRect(x: buttonInset, y: contentHeight - scanButtonHeight - insets.bottom, width: contentFrame.width, height: scanButtonHeight)) let doneButtonHeight = self.doneButton.updateLayout(width: contentFrame.width - buttonInset * 2.0, transition: transition) - transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom - 6.0, width: contentFrame.width, height: doneButtonHeight)) + transition.updateFrame(node: self.doneButton, frame: CGRect(x: buttonInset, y: contentHeight - doneButtonHeight - scanButtonHeight - 10.0 - insets.bottom, width: contentFrame.width, height: doneButtonHeight)) transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) @@ -1523,7 +1553,7 @@ private class ChatQrCodeScreenNode: ViewControllerTracingNode, ASScrollViewDeleg let contentSize = CGSize(width: contentFrame.width, height: 120.0) self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width) - self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 6.0) + self.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0 + titleHeight + 12.0) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.contentNode.updateLayout(size: layout.size, topInset: 44.0, bottomInset: contentHeight, transition: transition) diff --git a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift index 48dc56d531..cdf93e25d7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftAnimationComponent/Sources/GiftCompositionComponent.swift @@ -18,6 +18,10 @@ import UIKitRuntimeUtils public final class GiftCompositionComponent: Component { public class ExternalState { public fileprivate(set) var previewPatternColor: UIColor? + public fileprivate(set) var previewModel: StarGift.UniqueGift.Attribute? + public fileprivate(set) var previewBackdrop: StarGift.UniqueGift.Attribute? + public fileprivate(set) var previewSymbol: StarGift.UniqueGift.Attribute? + public init() { self.previewPatternColor = nil } @@ -671,18 +675,23 @@ public final class GiftCompositionComponent: Component { loop = true self.stopSpinIfNeeded() - if self.previewModels.isEmpty { + if self.previewModels.isEmpty || sampleAttributes.count == 3 { var models: [StarGift.UniqueGift.Attribute] = [] var patterns: [StarGift.UniqueGift.Attribute] = [] var backdrops: [StarGift.UniqueGift.Attribute] = [] for attribute in sampleAttributes { switch attribute { - case .model: models.append(attribute) + case .model: models.append(attribute) case .pattern: patterns.append(attribute) case .backdrop: backdrops.append(attribute) default: break } } + + if self.previewModels != models && sampleAttributes.count == 3 { + self.animatePreviewTransition = true + } + self.previewModels = models self.previewPatterns = patterns self.previewBackdrops = backdrops @@ -707,19 +716,22 @@ public final class GiftCompositionComponent: Component { } if case let .model(_, file, _) = self.previewModels[Int(self.previewModelIndex)] { animationFile = file + component.externalState?.previewModel = self.previewModels[Int(self.previewModelIndex)] } if case let .pattern(_, file, _) = self.previewPatterns[Int(self.previewPatternIndex)] { patternFile = file files[file.fileId.id] = file + component.externalState?.previewSymbol = self.previewPatterns[Int(self.previewPatternIndex)] } if case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _) = self.previewBackdrops[Int(self.previewBackdropIndex)] { backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue)) secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue)) patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) + component.externalState?.previewBackdrop = self.previewBackdrops[Int(self.previewBackdropIndex)] } } - if self.previewTimer == nil { + if self.previewTimer == nil && sampleAttributes.count > 3 { self.previewTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in guard let self, !self.previewModels.isEmpty else { return } self.previewModelIndex = (self.previewModelIndex + 1) % Int32(self.previewModels.count) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 6fc7466c93..4ceb2486c4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -30,6 +30,7 @@ public final class GiftItemComponent: Component { case starGift(gift: StarGift.Gift, price: String) case uniqueGift(gift: StarGift.UniqueGift, price: String?) case auction(gift: StarGift.Gift, centerColor: UIColor, edgeColor: UIColor, endTime: Int32) + case preview(attributes: [StarGift.UniqueGift.Attribute], rarity: Int32) } public struct Ribbon: Equatable { @@ -144,6 +145,7 @@ public final class GiftItemComponent: Component { case buttonIcon case tableIcon case header + case upgradePreview } let context: AccountContext @@ -421,6 +423,14 @@ public final class GiftItemComponent: Component { size = availableSize iconSize = CGSize(width: 106.0, height: 106.0) cornerRadius = 16.0 + case .upgradePreview: + size = availableSize + if case let .preview(attributes, _) = component.subject, attributes.count == 2 { + iconSize = CGSize(width: 60.0, height: 60.0) + } else { + iconSize = CGSize(width: 72.0, height: 72.0) + } + cornerRadius = 16.0 } var backgroundSize = size if case .grid = component.mode { @@ -565,6 +575,45 @@ public final class GiftItemComponent: Component { }, queue: Queue.mainQueue()) self.giftAuctionTimer?.start() } + case let .preview(attributes, _): + animationOffset = 16.0 + explicitAnimationOffset = -4.0 + for attribute in attributes { + switch attribute { + case let .model(_, file, _): + animationFile = file + if !self.fetchedFiles.contains(file.fileId.id) { + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: component.context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + self.fetchedFiles.insert(file.fileId.id) + } + case let .pattern(_, file, _): + patternFile = file + files[file.fileId.id] = file + case let .backdrop(_, _, innerColorValue, outerColorValue, patternColorValue, _, _): + backgroundColor = UIColor(rgb: UInt32(bitPattern: outerColorValue)) + secondBackgroundColor = UIColor(rgb: UInt32(bitPattern: innerColorValue)) + patternColor = UIColor(rgb: UInt32(bitPattern: patternColorValue)) + if let backgroundColor { + placeholderColor = backgroundColor + } + default: + break + } + } + + if animationFile == nil, let patternFile { + animationFile = patternFile + } + + if let animationFile { + emoji = ChatTextInputTextCustomEmojiAttribute( + interactivelySelectedFromPackId: nil, + fileId: animationFile.fileId.id, + file: animationFile + ) + } else { + emoji = nil + } } if [.buttonIcon, .tableIcon].contains(component.mode) { @@ -640,7 +689,62 @@ public final class GiftItemComponent: Component { } } - if case .preview = component.mode { + if case .upgradePreview = component.mode, case let .preview(attributes, rarity) = component.subject { + let isColored = attributes.count > 1 + if let title = component.title { + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.medium(13.0), textColor: isColored ? .white : component.theme.list.itemPrimaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: size.height - 27.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + } + + func formatPercentage(_ value: Float) -> String { + return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + "%" + } + let percentage = Float(rarity) * 0.1 + + let badgeTextSize = self.badgeText.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: formatPercentage(percentage), font: Font.with(size: 11.0, weight: .medium, traits: .monospacedNumbers), textColor: isColored ? .white : component.theme.list.itemSecondaryTextColor))) + ), + environment: {}, + containerSize: availableSize + ) + + let badgeBackgroundSize = CGSize(width: badgeTextSize.width + 12.0, height: 18.0) + let _ = self.badgeBackground.update( + transition: .spring(duration: 0.2), + component: AnyComponent( + RoundedRectangle(color: isColored ? UIColor(white: 0.0, alpha: 0.2) : component.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.06), cornerRadius: 9.0) + ), + environment: {}, + containerSize: badgeBackgroundSize + ) + + if let badgeBackgroundView = self.badgeBackground.view, let badgeTextView = self.badgeText.view { + if badgeBackgroundView.superview == nil { + self.addSubview(badgeBackgroundView) + self.addSubview(badgeTextView) + } + badgeTextView.frame = CGRect(origin: CGPoint(x: size.width - 12.0 - badgeTextSize.width, y: 9.0), size: badgeTextSize) + badgeBackgroundView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(badgeTextView.frame.center.x - badgeBackgroundSize.width / 2.0), y: floorToScreenPixels(badgeTextView.frame.center.y - badgeBackgroundSize.height / 2.0)), size: badgeBackgroundSize) + } + } else if case .preview = component.mode { if let title = component.title { let titleSize = self.title.update( transition: transition, @@ -793,7 +897,9 @@ public final class GiftItemComponent: Component { case .auction: buttonColor = .clear price = "" - break + case .preview: + buttonColor = .clear + price = "" } let buttonSize = self.button.update( @@ -1189,9 +1295,19 @@ public final class GiftItemComponent: Component { } switch component.mode { - case .generic, .grid: + case .generic, .grid, .upgradePreview: let lineWidth: CGFloat = 2.0 - let selectionFrame = backgroundFrame.insetBy(dx: 3.0, dy: 3.0) + let selectionFrame = backgroundFrame.insetBy(dx: 2.0, dy: 2.0) + + var cornerRadius: CGFloat = 6.0 + if case .upgradePreview = component.mode { + cornerRadius = 13.0 + } + + var selectionColor = UIColor.white + if case .upgradePreview = component.mode, case let .preview(attributes, _) = component.subject, attributes.count == 1 { + selectionColor = component.theme.list.itemAccentColor + } if component.isSelected { let selectionLayer: SimpleShapeLayer @@ -1207,13 +1323,13 @@ public final class GiftItemComponent: Component { } selectionLayer.fillColor = UIColor.clear.cgColor - selectionLayer.strokeColor = UIColor.white.cgColor + selectionLayer.strokeColor = selectionColor.cgColor 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) + selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, 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) + let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, 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) } @@ -1222,7 +1338,7 @@ public final class GiftItemComponent: Component { } else if let selectionLayer = self.selectionLayer { self.selectionLayer = nil - 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) + let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: cornerRadius, cornerHeight: cornerRadius, 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() diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD index 350b39f791..c4cde11b0e 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/BUILD @@ -61,6 +61,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/LiveChat/StoryLiveChatMessageComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/BotPaymentsUI", + "//submodules/TelegramUI/Components/SegmentControlComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 0ec246d3b5..07629d6fb4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -1141,8 +1141,7 @@ private final class GiftAuctionBidScreenComponent: Component { private let sliderPlus = ComponentView() private let badge = ComponentView() - private var liveStreamPerks: [ComponentView] = [] - private var liveStreamMessagePreview: ComponentView? + private var auctionStats: [ComponentView] = [] private let myGifts = ComponentView() @@ -2366,7 +2365,7 @@ private final class GiftAuctionBidScreenComponent: Component { self.badgeStars.update(size: starsRect.size, color: sliderColor, emitterPosition: CGPoint(x: badgeFrame.midX, y: badgeFrame.maxY - 32.0)) } - var perks: [([AnimatedTextComponent.Item], String)] = [] + var auctionStats: [([AnimatedTextComponent.Item], String)] = [] var minBidAnimatedItems: [AnimatedTextComponent.Item] = [] var untilNextDropAnimatedItems: [AnimatedTextComponent.Item] = [] @@ -2458,62 +2457,62 @@ private final class GiftAuctionBidScreenComponent: Component { } } - perks.append(( + auctionStats.append(( minBidAnimatedItems, environment.strings.Gift_AuctionBid_MinimumBid )) - perks.append(( + auctionStats.append(( untilNextDropAnimatedItems, environment.strings.Gift_AuctionBid_UntilNext )) - perks.append(( + auctionStats.append(( dropsLeftAnimatedItems, environment.strings.Gift_AuctionBid_Left )) contentHeight += 54.0 - let perkHeight: CGFloat = 60.0 - let perkSpacing: CGFloat = 10.0 - let perkWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - perkSpacing * CGFloat(perks.count - 1)) / CGFloat(perks.count)) + let statSpacing: CGFloat = 10.0 + let statWidth: CGFloat = floor((availableSize.width - sideInset * 2.0 - statSpacing * CGFloat(auctionStats.count - 1)) / CGFloat(auctionStats.count)) + let statHeight: CGFloat = 60.0 - for i in 0 ..< perks.count { - var perkFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (perkWidth + perkSpacing), y: contentHeight), size: CGSize(width: perkWidth, height: perkHeight)) - if i == perks.count - 1 { - perkFrame.size.width = max(0.0, availableSize.width - sideInset - perkFrame.minX) + for i in 0 ..< auctionStats.count { + var statFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (statWidth + statSpacing), y: contentHeight), size: CGSize(width: statWidth, height: statHeight)) + if i == auctionStats.count - 1 { + statFrame.size.width = max(0.0, availableSize.width - sideInset - statFrame.minX) } - let perkView: ComponentView - if self.liveStreamPerks.count > i { - perkView = self.liveStreamPerks[i] + let statView: ComponentView + if self.auctionStats.count > i { + statView = self.auctionStats[i] } else { - perkView = ComponentView() - self.liveStreamPerks.append(perkView) + statView = ComponentView() + self.auctionStats.append(statView) } - let perk = perks[i] - let _ = perkView.update( + let stat = auctionStats[i] + let _ = statView.update( transition: transition, component: AnyComponent(AuctionStatComponent( context: component.context, - gift: i == perks.count - 1 ? component.auctionContext.gift : nil, - title: perk.0, - subtitle: perk.1, + gift: i == auctionStats.count - 1 ? component.auctionContext.gift : nil, + title: stat.0, + subtitle: stat.1, small: false, theme: environment.theme )), environment: {}, - containerSize: perkFrame.size + containerSize: statFrame.size ) - if let perkComponentView = perkView.view { + if let perkComponentView = statView.view { if perkComponentView.superview == nil { self.scrollContentView.addSubview(perkComponentView) } - transition.setFrame(view: perkComponentView, frame: perkFrame) + transition.setFrame(view: perkComponentView, frame: statFrame) } } - contentHeight += perkHeight + contentHeight += statHeight contentHeight += 24.0 let acquiredGiftsCount = self.giftAuctionState?.myState.acquiredCount ?? 0 diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index b461cf3f60..9348390dbc 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -71,7 +71,10 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { fileprivate var giftAuctionAcquiredGifts: [GiftAuctionAcquiredGift] = [] private var giftAuctionAcquiredGiftsPromise = ValuePromise<[GiftAuctionAcquiredGift]>() private var giftAuctionAcquiredGiftsDisposable = MetaDisposable() - + + private(set) var giftValueInfo: StarGift.UniqueGift.ValueInfo? + private var giftValueInfoDisposable: Disposable? + var cachedStarImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? @@ -102,6 +105,17 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { if let acquiredCount = auctionState?.myState.acquiredCount, acquiredCount > (previousState?.myState.acquiredCount ?? 0) { self.loadAcquiredGifts() } + + if !"".isEmpty, self.giftValueInfoDisposable == nil, let auctionState, case let .generic(gift) = auctionState.gift, case .finished = auctionState.auctionState, let slug = gift.auctionSlug { + self.giftValueInfoDisposable = (self.context.engine.payments.getUniqueStarGiftValueInfo(slug: "\(slug)-1") + |> deliverOnMainQueue).start(next: { [weak self] valueInfo in + guard let self else { + return + } + self.giftValueInfo = valueInfo + self.updated(transition: .easeInOut(duration: 0.25)) + }) + } }) self.giftAuctionTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in @@ -114,6 +128,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { self.disposable?.dispose() self.giftAuctionAcquiredGiftsDisposable.dispose() self.giftAuctionTimer?.invalidate() + self.giftValueInfoDisposable?.dispose() } func loadAcquiredGifts() { @@ -291,6 +306,26 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { controller.present(shareController, in: .window(.root)) } + func openGiftResale() { + guard let controller = self.getController() as? GiftAuctionViewScreen, let gift = self.giftAuctionState?.gift, case let .generic(gift) = gift else { + return + } + let storeController = self.context.sharedContext.makeGiftStoreController( + context: self.context, + peerId: self.context.account.peerId, + gift: gift + ) + controller.push(storeController) + } + + func openGiftFragmentResale() { + guard let controller = self.getController() as? GiftAuctionViewScreen, let navigationController = controller.navigationController as? NavigationController, let url = self.giftValueInfo?.fragmentListedUrl else { + return + } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {}) + } + func morePressed(view: UIView, gesture: ContextGesture?) { guard let controller = self.getController() as? GiftAuctionViewScreen else { return @@ -367,8 +402,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let button = Child(ButtonComponent.self) let acquiredButton = Child(PlainButtonComponent.self) -// let telegramSaleButton = Child(PlainButtonComponent.self) -// let fragmentSaleButton = Child(PlainButtonComponent.self) + let telegramSaleButton = Child(PlainButtonComponent.self) + let fragmentSaleButton = Child(PlainButtonComponent.self) let moreButtonPlayOnce = ActionSlot() @@ -678,8 +713,13 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { guard let state else { return } + #if DEBUG + let giftController = GiftUpgradePreviewScreen(context: component.context) + environment.controller()?.push(giftController) + #else let giftController = GiftAuctionAcquiredScreen(context: component.context, gift: component.auctionContext.gift, acquiredGifts: state.giftAuctionAcquiredGifts) environment.controller()?.push(giftController) + #endif }, animateScale: false), availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), transition: context.transition @@ -692,6 +732,98 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { hasAdditionalButtons = true } + if let giftValueInfo = state.giftValueInfo, case let .generic(gift) = component.auctionContext.gift { + if let listedCount = giftValueInfo.listedCount, listedCount > 0 { + originY += 5.0 + + let telegramSaleButton = telegramSaleButton.update( + component: PlainButtonComponent(content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "count", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "spacing", component: AnyComponent( + Rectangle(color: .clear, width: 8.0, height: 1.0) + )), + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: .starGift(gift: gift, price: ""), + mode: .buttonIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnTelegram)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) + )) + ], spacing: 0.0) + ), action: { [weak state] in + guard let state else { + return + } + state.openGiftResale() + }, animateScale: false), + availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(telegramSaleButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + telegramSaleButton.size.height / 2.0))) + originY += telegramSaleButton.size.height + originY += 12.0 + + hasAdditionalButtons = true + } + + if let listedCount = giftValueInfo.fragmentListedCount, listedCount > 0 { + let fragmentSaleButton = fragmentSaleButton.update( + component: PlainButtonComponent(content: AnyComponent( + HStack([ + AnyComponentWithIdentity(id: "count", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(listedCount, dateTimeFormat.groupingSeparator), font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "spacing", component: AnyComponent( + Rectangle(color: .clear, width: 8.0, height: 1.0) + )), + AnyComponentWithIdentity(id: "icon", component: AnyComponent( + GiftItemComponent( + context: component.context, + theme: theme, + strings: strings, + peer: nil, + subject: .starGift(gift: gift, price: ""), + mode: .buttonIcon + ) + )), + AnyComponentWithIdentity(id: "text", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: " \(strings.Gift_Value_ForSaleOnFragment)", font: Font.regular(17.0), textColor: theme.actionSheet.controlAccentColor))) + )), + AnyComponentWithIdentity(id: "arrow", component: AnyComponent( + BundleIconComponent(name: "Chat/Context Menu/Arrow", tintColor: theme.actionSheet.controlAccentColor) + )) + ], spacing: 0.0) + ), action: { [weak state] in + guard let state else { + return + } + state.openGiftFragmentResale() + }, animateScale: false), + availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), + transition: context.transition + ) + context.add(fragmentSaleButton + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + fragmentSaleButton.size.height / 2.0))) + originY += fragmentSaleButton.size.height + originY += 12.0 + + hasAdditionalButtons = true + } + } + if hasAdditionalButtons { originY += 21.0 } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift new file mode 100644 index 0000000000..98942c36a1 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftOfferAlertController.swift @@ -0,0 +1,485 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import AccountContext +import AppBundle +import AvatarNode +import Markdown +import GiftItemComponent +import ChatMessagePaymentAlertController +import ActivityIndicator +import TooltipUI +import MultilineTextComponent +import TelegramStringFormatting + +private final class GiftOfferAlertContentNode: AlertContentNode { + private let context: AccountContext + private let strings: PresentationStrings + private var presentationTheme: PresentationTheme + private let title: String + private let text: String + private let gift: StarGift.UniqueGift + + private let titleNode: ASTextNode + private let giftView = ComponentView() + private let textNode: ASTextNode + private let arrowNode: ASImageNode + private let avatarNode: AvatarNode + private let tableView = ComponentView() + + private let modelButtonTag = GenericComponentViewTag() + private let backdropButtonTag = GenericComponentViewTag() + private let symbolButtonTag = GenericComponentViewTag() + + fileprivate var getController: () -> ViewController? = { return nil} + + private let actionNodesSeparator: ASDisplayNode + private let actionNodes: [TextAlertContentActionNode] + private let actionVerticalSeparators: [ASDisplayNode] + + private var activityIndicator: ActivityIndicator? + + private var validLayout: CGSize? + + var inProgress = false { + didSet { + if let size = self.validLayout { + let _ = self.updateLayout(size: size, transition: .immediate) + } + } + } + + override var dismissOnOutsideTap: Bool { + return self.isUserInteractionEnabled + } + + init( + context: AccountContext, + theme: AlertControllerTheme, + ptheme: PresentationTheme, + strings: PresentationStrings, + gift: StarGift.UniqueGift, + peer: EnginePeer, + title: String, + text: String, + actions: [TextAlertAction] + ) { + self.context = context + self.strings = strings + self.presentationTheme = ptheme + self.title = title + self.text = text + self.gift = gift + + self.titleNode = ASTextNode() + self.titleNode.maximumNumberOfLines = 0 + + self.textNode = ASTextNode() + self.textNode.maximumNumberOfLines = 0 + + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.displayWithoutProcessing = true + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) + + self.actionNodesSeparator = ASDisplayNode() + self.actionNodesSeparator.isLayerBacked = true + + self.actionNodes = actions.map { action -> TextAlertContentActionNode in + return TextAlertContentActionNode(theme: theme, action: action) + } + + var actionVerticalSeparators: [ASDisplayNode] = [] + if actions.count > 1 { + for _ in 0 ..< actions.count - 1 { + let separatorNode = ASDisplayNode() + separatorNode.isLayerBacked = true + actionVerticalSeparators.append(separatorNode) + } + } + self.actionVerticalSeparators = actionVerticalSeparators + + super.init() + + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.avatarNode) + + self.addSubnode(self.actionNodesSeparator) + + for actionNode in self.actionNodes { + self.addSubnode(actionNode) + } + + for separatorNode in self.actionVerticalSeparators { + self.addSubnode(separatorNode) + } + + self.updateTheme(theme) + + self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer) + } + + override func updateTheme(_ theme: AlertControllerTheme) { + self.titleNode.attributedText = NSAttributedString(string: self.title, font: Font.semibold(17.0), textColor: theme.primaryColor) + self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), + link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), + linkAttribute: { url in + return ("URL", url) + } + ), textAlignment: .center) + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor.withAlphaComponent(0.9)) + + self.actionNodesSeparator.backgroundColor = theme.separatorColor + for actionNode in self.actionNodes { + actionNode.updateTheme(theme) + } + for separatorNode in self.actionVerticalSeparators { + separatorNode.backgroundColor = theme.separatorColor + } + + if let size = self.validLayout { + _ = self.updateLayout(size: size, transition: .immediate) + } + } + + fileprivate func dismissAllTooltips() { + guard let controller = self.getController() else { + return + } + controller.window?.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + }) + controller.forEachController({ controller in + if let controller = controller as? TooltipScreen { + controller.dismiss(inPlace: false) + } + return true + }) + } + + func showAttributeInfo(tag: Any, text: String) { + guard let controller = self.getController() else { + return + } + self.dismissAllTooltips() + + guard let sourceView = self.tableView.findTaggedView(tag: tag), let absoluteLocation = sourceView.superview?.convert(sourceView.center, to: controller.view) else { + return + } + + let location = CGRect(origin: CGPoint(x: absoluteLocation.x, y: absoluteLocation.y - 12.0), size: CGSize()) + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: .plain(text: text), style: .wide, location: .point(location, .bottom), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) + }) + controller.present(tooltipController, in: .current) + } + + override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + var size = size + size.width = min(size.width, 310.0) + + let strings = self.strings + + self.validLayout = size + + var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) + + let avatarSize = CGSize(width: 60.0, height: 60.0) + self.avatarNode.updateSize(size: avatarSize) + + let giftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) + + let _ = self.giftView.update( + transition: .immediate, + component: AnyComponent( + GiftItemComponent( + context: self.context, + theme: self.presentationTheme, + strings: strings, + peer: nil, + subject: .uniqueGift(gift: self.gift, price: nil), + mode: .thumbnail + ) + ), + environment: {}, + containerSize: avatarSize + ) + if let view = self.giftView.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = giftFrame + } + + if let arrowImage = self.arrowNode.image { + let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) + transition.updateFrame(node: self.arrowNode, frame: arrowFrame) + } + + let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) + transition.updateFrame(node: self.avatarNode, frame: avatarFrame) + + origin.y += avatarSize.height + 17.0 + + let titleSize = self.titleNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: origin.y), size: titleSize)) + origin.y += titleSize.height + 5.0 + + let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) + transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) + origin.y += textSize.height + 10.0 + + let actionButtonHeight: CGFloat = 44.0 + var minActionsWidth: CGFloat = 0.0 + let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) + let actionTitleInsets: CGFloat = 8.0 + + for actionNode in self.actionNodes { + let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) + minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) + } + + let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) + + let contentWidth = max(size.width, minActionsWidth) + + let actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) + + let tableFont = Font.regular(15.0) + let tableTextColor = self.presentationTheme.list.itemPrimaryTextColor + + var tableItems: [TableComponent.Item] = [] + let order: [StarGift.UniqueGift.Attribute.AttributeType] = [ + .model, .pattern, .backdrop, .originalInfo + ] + + var attributeMap: [StarGift.UniqueGift.Attribute.AttributeType: StarGift.UniqueGift.Attribute] = [:] + for attribute in self.gift.attributes { + attributeMap[attribute.attributeType] = attribute + } + + for type in order { + if let attribute = attributeMap[type] { + let id: String? + let title: String? + let value: NSAttributedString + let percentage: Float? + let tag: AnyObject? + + switch attribute { + case let .model(name, _, rarity): + id = "model" + title = strings.Gift_Unique_Model + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity) * 0.1 + tag = self.modelButtonTag + case let .backdrop(name, _, _, _, _, _, rarity): + id = "backdrop" + title = strings.Gift_Unique_Backdrop + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity) * 0.1 + tag = self.backdropButtonTag + case let .pattern(name, _, rarity): + id = "pattern" + title = strings.Gift_Unique_Symbol + value = NSAttributedString(string: name, font: tableFont, textColor: tableTextColor) + percentage = Float(rarity) * 0.1 + tag = self.symbolButtonTag + case .originalInfo: + continue + } + + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: AnyHashable(0), + component: AnyComponent( + MultilineTextComponent(text: .plain(value)) + ) + ) + ) + if let percentage, let tag { + items.append(AnyComponentWithIdentity( + id: AnyHashable(1), + component: AnyComponent(Button( + content: AnyComponent(ButtonContentComponent( + context: self.context, + text: formatPercentage(percentage), + color: self.presentationTheme.list.itemAccentColor + )), + action: { [weak self] in + self?.showAttributeInfo(tag: tag, text: strings.Gift_Unique_AttributeDescription(formatPercentage(percentage)).string) + } + ).tagged(tag)) + )) + } + let itemComponent = AnyComponent( + HStack(items, spacing: 4.0) + ) + + tableItems.append(.init( + id: id, + title: title, + hasBackground: false, + component: itemComponent + )) + } + } + + if let valueAmount = self.gift.valueAmount, let valueCurrency = self.gift.valueCurrency { + tableItems.append(.init( + id: "fiatValue", + title: strings.Gift_Unique_Value, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))) + ), + insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) + )) + } + + let tableSize = self.tableView.update( + transition: .immediate, + component: AnyComponent( + TableComponent( + theme: self.presentationTheme, + items: tableItems, + semiTransparent: true + ) + ), + environment: {}, + containerSize: CGSize(width: contentWidth - 32.0, height: size.height) + ) + let tableFrame = CGRect(origin: CGPoint(x: 16.0, y: avatarSize.height + titleSize.height + textSize.height + 60.0), size: tableSize) + if let view = self.tableView.view { + if view.superview == nil { + self.view.addSubview(view) + } + view.frame = tableFrame + } + + let resultSize = CGSize(width: contentWidth, height: avatarSize.height + titleSize.height + textSize.height + tableSize.height + actionsHeight + 40.0 + insets.top + insets.bottom) + transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + + var actionOffset: CGFloat = 0.0 + var separatorIndex = -1 + var nodeIndex = 0 + for actionNode in self.actionNodes { + if separatorIndex >= 0 { + let separatorNode = self.actionVerticalSeparators[separatorIndex] + do { + transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) + } + } + separatorIndex += 1 + + let currentActionWidth: CGFloat + do { + currentActionWidth = resultSize.width + } + + let actionNodeFrame: CGRect + do { + actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) + actionOffset += actionButtonHeight + } + + transition.updateFrame(node: actionNode, frame: actionNodeFrame) + + nodeIndex += 1 + } + + if self.inProgress { + let activityIndicator: ActivityIndicator + if let current = self.activityIndicator { + activityIndicator = current + } else { + activityIndicator = ActivityIndicator(type: .custom(self.presentationTheme.list.freeInputField.controlColor, 18.0, 1.5, false)) + self.addSubnode(activityIndicator) + } + + if let actionNode = self.actionNodes.first { + actionNode.isUserInteractionEnabled = false + actionNode.isHidden = false + + let indicatorSize = CGSize(width: 22.0, height: 22.0) + transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: actionNode.frame.minX + floor((actionNode.frame.width - indicatorSize.width) / 2.0), y: actionNode.frame.minY + floor((actionNode.frame.height - indicatorSize.height) / 2.0)), size: indicatorSize)) + } + } + + return resultSize + } +} + +public func giftOfferAlertController( + context: AccountContext, + gift: StarGift.UniqueGift, + peer: EnginePeer, + amount: CurrencyAmount, + commit: @escaping () -> Void +) -> AlertController { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let strings = presentationData.strings + + let title = "Confirm Sale" + let buttonText: String = "Confirm Sale" + + let priceString: String + switch amount.currency { + case .stars: + priceString = "\(amount.amount) Stars" + case .ton: + priceString = "\(amount.amount) TON" + } + + let resaleConfiguration = StarsSubscriptionConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let finalPriceString: String + switch amount.currency { + case .stars: + let starsValue = Int32(floor(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionStarsPermille) / 1000.0)) + finalPriceString = "\(starsValue) Stars" + case .ton: + let tonValue = Int64(Float(amount.amount.value) * Float(resaleConfiguration.starGiftCommissionTonPermille) / 1000.0) + finalPriceString = formatTonAmountText(tonValue, dateTimeFormat: presentationData.dateTimeFormat, maxDecimalPositions: 3) + " TON" + } + + let giftTitle = "\(gift.title) #\(formatCollectibleNumber(gift.number, dateTimeFormat: presentationData.dateTimeFormat))" + let text = "Do you want to sell **\(giftTitle)** to \(peer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)) for **\(priceString)**? You'll receive **\(finalPriceString)** after fees." + + + var contentNode: GiftOfferAlertContentNode? + var dismissImpl: ((Bool) -> Void)? + let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: buttonText, action: { [weak contentNode] in + contentNode?.inProgress = true + commit() + }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + dismissImpl?(true) + })] + + contentNode = GiftOfferAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, gift: gift, peer: peer, title: title, text: text, actions: actions) + + let controller = ChatMessagePaymentAlertController(context: context, presentationData: presentationData, contentNode: contentNode!, navigationController: nil, chatPeerId: context.account.peerId, showBalance: false) + contentNode?.getController = { [weak controller] in + return controller + } + dismissImpl = { [weak controller] animated in + if animated { + controller?.dismissAnimated() + } else { + controller?.dismiss() + } + } + return controller +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift index 45585138f6..a58d8bf55c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftTransferAlertController.swift @@ -344,7 +344,7 @@ private final class GiftTransferAlertContentNode: AlertContentNode { id: "fiatValue", title: strings.Gift_Unique_Value, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: "≈\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor))) ), insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0) )) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift index e02517851e..43a1b83264 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUnpinScreen.swift @@ -16,6 +16,7 @@ import ButtonComponent import PlainButtonComponent import GiftItemComponent import AccountContext +import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -62,8 +63,7 @@ private final class SheetContent: CombinedComponent { } static var body: Body { - let closeButton = Child(Button.self) - + let closeButton = Child(GlassBarButtonComponent.self) let title = Child(BalancedTextComponent.self) let text = Child(BalancedTextComponent.self) let gifts = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) @@ -79,7 +79,6 @@ private final class SheetContent: CombinedComponent { let theme = environment.theme let strings = environment.strings - let sideInset: CGFloat = 16.0 + environment.safeInsets.left let textSideInset: CGFloat = 32.0 + environment.safeInsets.left let titleFont = Font.semibold(17.0) @@ -87,20 +86,29 @@ private final class SheetContent: CombinedComponent { let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor - var contentSize = CGSize(width: context.availableSize.width, height: 10.0) + var contentSize = CGSize(width: context.availableSize.width, height: 18.0) let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Text(text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.actionSheet.controlAccentColor)), - action: { [weak component] in - component?.dismiss() + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { _ in + component.dismiss() } ), - availableSize: CGSize(width: 100.0, height: 30.0), + availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 28.0)) + .position(CGPoint(x: environment.safeInsets.left + 16.0 + closeButton.size.width / 2.0, y: 36.0)) ) let title = title.update( @@ -234,6 +242,7 @@ private final class SheetContent: CombinedComponent { let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) @@ -256,7 +265,7 @@ private final class SheetContent: CombinedComponent { } } ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), transition: context.transition ) context.add(button @@ -335,6 +344,7 @@ private final class SheetContainerComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, externalState: sheetExternalState, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift new file mode 100644 index 0000000000..2efb882263 --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftUpgradePreviewScreen.swift @@ -0,0 +1,1381 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import TelegramPresentationData +import ComponentFlow +import AccountContext +import ViewControllerComponent +import TelegramCore +import SwiftSignalKit +import Display +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import ButtonComponent +import PlainButtonComponent +import Markdown +import BundleIconComponent +import TextFormat +import TelegramStringFormatting +import GlassBarButtonComponent +import GiftItemComponent +import EdgeEffect +import AnimatedTextComponent +import SegmentControlComponent +import GiftAnimationComponent +import GlassBackgroundComponent + +private final class GiftUpgradePreviewScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + + init( + context: AccountContext + ) { + self.context = context + } + + static func ==(lhs: GiftUpgradePreviewScreenComponent, rhs: GiftUpgradePreviewScreenComponent) -> Bool { + return true + } + + private struct ItemLayout: Equatable { + var containerSize: CGSize + var containerInset: CGFloat + var containerCornerRadius: CGFloat + var bottomInset: CGFloat + var topInset: CGFloat + + init(containerSize: CGSize, containerInset: CGFloat, containerCornerRadius: CGFloat, bottomInset: CGFloat, topInset: CGFloat) { + self.containerSize = containerSize + self.containerInset = containerInset + self.containerCornerRadius = containerCornerRadius + self.bottomInset = bottomInset + self.topInset = topInset + } + } + + enum SelectedSection { + case models + case backdrops + case symbols + } + + private final class ScrollView: UIScrollView { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return super.hitTest(point, with: event) + } + } + + final class View: UIView, UIScrollViewDelegate { + private let dimView: UIView + private let containerView: UIView + private let backgroundLayer: SimpleLayer + private let navigationBarContainer: SparseContainerView + private let glassContainerView: GlassBackgroundContainerView + private let scrollView: ScrollView + private let scrollContentClippingView: SparseContainerView + private let scrollContentView: UIView + + private let backgroundHandleView: UIImageView + + private let header = ComponentView() + private let closeButton = ComponentView() + private let playbackButton = ComponentView() + + private let title = ComponentView() + private let subtitle = ComponentView() + + private var attributeInfos: [ComponentView] = [] + + private let segmentControl = ComponentView() + private let descriptionText = ComponentView() + + private var giftItems: [AnyHashable: ComponentView] = [:] + + private var selectedSection: SelectedSection = .models + + private let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + + fileprivate var starGiftsContext: ResaleGiftsContext? + fileprivate var starGiftsState: ResaleGiftsContext.State? + fileprivate var starGiftsDisposable: Disposable? + + private var previewTimer: SwiftSignalKit.Timer? + private var isPlaying = true + private var showRandomizeTip = false + + private var previewModelIndex: Int = 0 + private var previewBackdropIndex: Int = 0 + private var previewSymbolIndex: Int = 0 + + private var previewModels: [StarGift.UniqueGift.Attribute] = [] + private var previewBackdrops: [StarGift.UniqueGift.Attribute] = [] + private var previewSymbols: [StarGift.UniqueGift.Attribute] = [] + + private var selectedModel: StarGift.UniqueGift.Attribute? + private var selectedBackdrop: StarGift.UniqueGift.Attribute? + private var selectedSymbol: StarGift.UniqueGift.Attribute? + + private var modelCount: Int32 = 0 + private var backdropCount: Int32 = 0 + private var symbolCount: Int32 = 0 + + private var ignoreScrolling: Bool = false + + private var component: GiftUpgradePreviewScreenComponent? + private weak var state: EmptyComponentState? + private var isUpdating: Bool = false + private var environment: ViewControllerComponentContainer.Environment? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.dimView = UIView() + self.containerView = UIView() + + self.containerView.clipsToBounds = true + self.containerView.layer.cornerRadius = 40.0 + self.containerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + + self.backgroundLayer = SimpleLayer() + self.backgroundLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + self.backgroundLayer.cornerRadius = 40.0 + + self.backgroundHandleView = UIImageView() + + self.navigationBarContainer = SparseContainerView() + + self.glassContainerView = GlassBackgroundContainerView() + + self.scrollView = ScrollView() + + self.scrollContentClippingView = SparseContainerView() + self.scrollContentClippingView.clipsToBounds = true + + self.scrollContentView = UIView() + + super.init(frame: frame) + + self.addSubview(self.dimView) + self.addSubview(self.containerView) + self.containerView.layer.addSublayer(self.backgroundLayer) + + self.scrollView.delaysContentTouches = true + self.scrollView.canCancelContentTouches = true + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.alwaysBounceVertical = true + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.containerView.addSubview(self.scrollContentClippingView) + self.scrollContentClippingView.addSubview(self.scrollView) + + self.scrollView.addSubview(self.scrollContentView) + + self.containerView.addSubview(self.navigationBarContainer) + + self.dimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.starGiftsDisposable?.dispose() + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateScrolling(transition: .immediate) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.bounds.contains(point) { + return nil + } + if !self.backgroundLayer.frame.contains(point) { + return self.dimView + } + + if let result = self.navigationBarContainer.hitTest(self.convert(point, to: self.navigationBarContainer), with: event) { + return result + } + let result = super.hitTest(point, with: event) + return result + } + + @objc private func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + guard let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + } + + private func updateScrolling(transition: ComponentTransition) { + guard let itemLayout = self.itemLayout else { + return + } + var topOffset = -self.scrollView.bounds.minY + itemLayout.topInset + topOffset = max(0.0, topOffset) + transition.setTransform(layer: self.backgroundLayer, transform: CATransform3DMakeTranslation(0.0, topOffset + itemLayout.containerInset, 0.0)) + + transition.setPosition(view: self.navigationBarContainer, position: CGPoint(x: 0.0, y: topOffset + itemLayout.containerInset)) + + var topOffsetFraction = self.scrollView.bounds.minY / 100.0 + topOffsetFraction = max(0.0, min(1.0, topOffsetFraction)) + + let minScale: CGFloat = (itemLayout.containerSize.width - 6.0 * 2.0) / itemLayout.containerSize.width + let minScaledTranslation: CGFloat = (itemLayout.containerSize.height - itemLayout.containerSize.height * minScale) * 0.5 - 6.0 + let minScaledCornerRadius: CGFloat = itemLayout.containerCornerRadius + + let scale = minScale * (1.0 - topOffsetFraction) + 1.0 * topOffsetFraction + let scaledTranslation = minScaledTranslation * (1.0 - topOffsetFraction) + let scaledCornerRadius = minScaledCornerRadius * (1.0 - topOffsetFraction) + itemLayout.containerCornerRadius * topOffsetFraction + + var containerTransform = CATransform3DIdentity + containerTransform = CATransform3DTranslate(containerTransform, 0.0, scaledTranslation, 0.0) + containerTransform = CATransform3DScale(containerTransform, scale, scale, scale) + transition.setTransform(view: self.containerView, transform: containerTransform) + transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: scaledCornerRadius) + + self.updateItems(transition: transition) + } + + func animateIn() { + self.dimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + let animateOffset: CGFloat = self.bounds.height - self.backgroundLayer.frame.minY + + self.dimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.scrollContentClippingView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true, completion: { _ in + completion() + }) + self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) + } + + private func timerTick() { + guard !self.previewModels.isEmpty else { return } + self.previewModelIndex = (self.previewModelIndex + 1) % self.previewModels.count + + let previousSymbolIndex = self.previewSymbolIndex + var randomSymbolIndex = previousSymbolIndex + while randomSymbolIndex == previousSymbolIndex && !self.previewSymbols.isEmpty { + randomSymbolIndex = Int.random(in: 0 ..< self.previewSymbols.count) + } + if !self.previewSymbols.isEmpty { self.previewSymbolIndex = randomSymbolIndex } + + let previousBackdropIndex = self.previewBackdropIndex + var randomBackdropIndex = previousBackdropIndex + while randomBackdropIndex == previousBackdropIndex && !self.previewBackdrops.isEmpty { + randomBackdropIndex = Int.random(in: 0 ..< self.previewBackdrops.count) + } + if !self.previewBackdrops.isEmpty { self.previewBackdropIndex = randomBackdropIndex } + + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + + private func updateTimer() { + if self.isPlaying { + self.previewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in + guard let self else { + return + } + self.timerTick() + }, queue: Queue.mainQueue()) + self.previewTimer?.start() + } else { + self.previewTimer?.invalidate() + self.previewTimer = nil + } + } + + private var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] + private func updateEffectiveGifts() { + guard let starGiftsState = self.starGiftsState else { + return + } + + var effectiveGifts: [[StarGift.UniqueGift.Attribute]] = [] + switch self.selectedSection { + case .models: + let models = Array(starGiftsState.attributes.filter({ attribute in + if case .model = attribute { + return true + } else { + return false + } + })) + for model in models { + effectiveGifts.append([model]) + } + case .backdrops: + let selectedModel = self.selectedModel ?? self.previewModels[self.previewModelIndex] + let selectedSymbol = self.selectedSymbol ?? self.previewSymbols[self.previewSymbolIndex] + let backdrops = Array(starGiftsState.attributes.filter({ attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + })) + for backdrop in backdrops { + effectiveGifts.append([ + selectedModel, + backdrop, + selectedSymbol + ]) + } + case .symbols: + let selectedBackdrop = self.selectedBackdrop ?? self.previewBackdrops[self.previewBackdropIndex] + let symbols = Array(starGiftsState.attributes.filter({ attribute in + if case .pattern = attribute { + return true + } else { + return false + } + })) + for symbol in symbols { + effectiveGifts.append([ + selectedBackdrop, + symbol + ]) + } + } + self.effectiveGifts = effectiveGifts + } + + private func updateItems(transition: ComponentTransition) { + guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { + return + } + + let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -10.0) + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + let optionSpacing: CGFloat = 10.0 + let optionWidth = (itemLayout.containerSize.width - sideInset * 2.0 - optionSpacing * 2.0) / 3.0 + let optionSize = CGSize(width: optionWidth, height: 126.0) + + let topInset: CGFloat = 393.0 + + var validIds: [AnyHashable] = [] + var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset + 9.0), size: optionSize) + + for attributeList in self.effectiveGifts { + var isVisible = false + if visibleBounds.intersects(itemFrame) { + isVisible = true + } + + var itemId = "" + var title = "" + var rarity: Int32 = 0 + + var modelAttribute: StarGift.UniqueGift.Attribute? + var backdropAttribute: StarGift.UniqueGift.Attribute? + var symbolAttribute: StarGift.UniqueGift.Attribute? + + switch self.selectedSection { + case .models: + itemId += "models_" + case .backdrops: + itemId += "backdrops_" + case .symbols: + itemId += "symbols_" + } + + var isSelected = false + for attribute in attributeList { + switch attribute { + case let .model(name, _, rarityValue): + itemId += name + if self.selectedSection == .models { + title = name + rarity = rarityValue + modelAttribute = attribute + + isSelected = self.selectedModel == attribute + } + case let .backdrop(name, _, _, _, _, _, rarityValue): + itemId += name + if self.selectedSection == .backdrops { + title = name + rarity = rarityValue + backdropAttribute = attribute + + isSelected = self.selectedBackdrop == attribute + } + case let .pattern(name, _, rarityValue): + itemId += name + if self.selectedSection == .symbols { + title = name + rarity = rarityValue + symbolAttribute = attribute + + isSelected = self.selectedSymbol == attribute + } + default: + break + } + } + + if isVisible { + validIds.append(itemId) + + var itemTransition = transition + let visibleItem: ComponentView + if let current = self.giftItems[itemId] { + visibleItem = current + } else { + visibleItem = ComponentView() + if !transition.animation.isImmediate { + itemTransition = .immediate + } + self.giftItems[itemId] = visibleItem + } + + let subject: GiftItemComponent.Subject = .preview(attributes: attributeList, rarity: rarity) + let _ = visibleItem.update( + transition: itemTransition, + component: AnyComponent( + PlainButtonComponent( + content: AnyComponent( + GiftItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + peer: nil, + subject: subject, + title: title, + ribbon: nil, + isSelected: isSelected, + mode: .upgradePreview + ) + ), + effectAlignment: .center, + action: { [weak self] in + guard let self, let state = self.state else { + return + } + if self.isPlaying { + self.isPlaying = false + self.showRandomizeTip = true + Queue.mainQueue().after(2.0) { + if self.showRandomizeTip { + self.showRandomizeTip = false + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + } + } + + switch self.selectedSection { + case .models: + self.selectedModel = modelAttribute + case .backdrops: + self.selectedBackdrop = backdropAttribute + case .symbols: + self.selectedSymbol = symbolAttribute + } + + state.updated(transition: .easeInOut(duration: 0.25)) + }, + animateAlpha: false + ) + ), + environment: {}, + containerSize: optionSize + ) + if let itemView = visibleItem.view { + if itemView.superview == nil { + self.scrollContentView.addSubview(itemView) + + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.25) + itemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + } + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + } + itemFrame.origin.x += itemFrame.width + optionSpacing + if itemFrame.maxX > itemLayout.containerSize.width { + itemFrame.origin.x = sideInset + itemFrame.origin.y += optionSize.height + optionSpacing + } + } + + var removeIds: [AnyHashable] = [] + for (id, item) in self.giftItems { + if !validIds.contains(id) { + removeIds.append(id) + if let itemView = item.view { + if !transition.animation.isImmediate { + itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false) + itemView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in + itemView.removeFromSuperview() + }) + } else { + itemView.removeFromSuperview() + } + } + } + } + for id in removeIds { + self.giftItems.removeValue(forKey: id) + } + } + + func update(component: GiftUpgradePreviewScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + self.updateTimer() + + let environment = environment[ViewControllerComponentContainer.Environment.self].value + let themeUpdated = self.environment?.theme !== environment.theme + + let resetScrolling = self.scrollView.bounds.width != availableSize.width + + let fillingSize: CGFloat + if case .regular = environment.metrics.widthClass { + fillingSize = min(availableSize.width, 414.0) - environment.safeInsets.left * 2.0 + } else { + fillingSize = min(availableSize.width, environment.deviceMetrics.screenSize.width) - environment.safeInsets.left * 2.0 + } + let rawSideInset: CGFloat = floor((availableSize.width - fillingSize) * 0.5) + let sideInset: CGFloat = rawSideInset + 16.0 + + if self.component == nil { + let giftIds: [Int64] = [ + 6014675319464657779, + 6042113507581755979, + 5936013938331222567 + ] + + self.starGiftsContext = ResaleGiftsContext(account: component.context.account, giftId: giftIds.randomElement()!) + self.starGiftsDisposable = (self.starGiftsContext!.state + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let self else { + return + } + let isFirstTime = self.starGiftsState?.attributes.isEmpty ?? true + self.starGiftsState = state + + var modelCount: Int32 = 0 + var backdropCount: Int32 = 0 + var symbolCount: Int32 = 0 + for attribute in state.attributes { + switch attribute { + case .model: + modelCount += 1 + case .backdrop: + backdropCount += 1 + case .pattern: + symbolCount += 1 + default: + break + } + } + self.modelCount = modelCount + self.backdropCount = backdropCount + self.symbolCount = symbolCount + + if isFirstTime { + let randomModels = Array(state.attributes.filter({ attribute in + if case .model = attribute { + return true + } else { + return false + } + }).shuffled().prefix(15)) + self.previewModels = randomModels + + let randomBackdrops = Array(state.attributes.filter({ attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }).shuffled()) + self.previewBackdrops = randomBackdrops + + let randomSymbols = Array(state.attributes.filter({ attribute in + if case .pattern = attribute { + return true + } else { + return false + } + }).shuffled().prefix(15)) + self.previewSymbols = randomSymbols + + self.updateEffectiveGifts() + } + + self.state?.updated(transition: .immediate) + }) + } + + self.component = component + self.state = state + self.environment = environment + + let theme = environment.theme.withModalBlocksBackground() + + if themeUpdated { + self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + self.backgroundLayer.backgroundColor = theme.list.blocksBackgroundColor.cgColor + } + + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + var buttonColor: UIColor = .white.withAlphaComponent(0.1) + var secondaryTextColor: UIColor = .white.withAlphaComponent(0.4) + + var attributes: [StarGift.UniqueGift.Attribute] = [] + if !self.previewModels.isEmpty { + if self.isPlaying { + attributes.append(self.previewModels[self.previewModelIndex]) + attributes.append(self.previewBackdrops[self.previewBackdropIndex]) + attributes.append(self.previewSymbols[self.previewSymbolIndex]) + } else { + if self.selectedModel == nil { + self.selectedModel = self.previewModels[self.previewModelIndex] + } + if self.selectedBackdrop == nil { + self.selectedBackdrop = self.previewBackdrops[self.previewBackdropIndex] + } + if self.selectedSymbol == nil { + self.selectedSymbol = self.previewSymbols[self.previewSymbolIndex] + } + if let model = self.selectedModel { + attributes.append(model) + } + if let backdrop = self.selectedBackdrop { + attributes.append(backdrop) + } + if let symbol = self.selectedSymbol { + attributes.append(symbol) + } + } + } + + if let backdropAttribute = attributes.first(where: { attribute in + if case .backdrop = attribute { + return true + } else { + return false + } + }), case let .backdrop(_, _, innerColor, outerColor, _, _, _) = backdropAttribute { + let topColor = UIColor(rgb: UInt32(bitPattern: innerColor)).withMultiplied(hue: 1.01, saturation: 1.22, brightness: 1.04) + let bottomColor = UIColor(rgb: UInt32(bitPattern: outerColor)).withMultiplied(hue: 0.97, saturation: 1.45, brightness: 0.89) + buttonColor = topColor.mixedWith(bottomColor, alpha: 0.8) + + secondaryTextColor = topColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) + } + + var contentHeight: CGFloat = 0.0 + let headerSize = self.header.update( + transition: transition, + component: AnyComponent(GiftCompositionComponent( + context: component.context, + theme: environment.theme, + subject: .preview(attributes), + animationOffset: CGPoint(x: 0.0, y: 20.0), + animationScale: nil, + displayAnimationStars: false, + revealedAttributes: Set(), + externalState: self.giftCompositionExternalState, + requestUpdate: { [weak state] transition in + state?.updated(transition: transition) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 300.0), + ) + let headerFrame = CGRect(origin: CGPoint(x: floor((fillingSize - headerSize.width) * 0.5), y: 0.0), size: headerSize) + if let headerView = self.header.view { + if headerView.superview == nil { + headerView.isUserInteractionEnabled = false + headerView.clipsToBounds = true + headerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + headerView.layer.cornerRadius = 38.0 + self.navigationBarContainer.addSubview(headerView) + } + transition.setFrame(view: headerView, frame: headerFrame) + } + + contentHeight += headerSize.height + + var titleText: String = "" + if let gift = self.starGiftsState?.gifts.first, case let .unique(gift) = gift { + titleText = gift.title + } + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: titleText, font: Font.semibold(20.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((fillingSize - titleSize.width) * 0.5), y: contentHeight - 124.0), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.navigationBarContainer.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + let subtitleSize = self.subtitle.update( + transition: .spring(duration: 0.2), + component: AnyComponent(AnimatedTextComponent( + font: Font.regular(14.0), + color: secondaryTextColor, + items: [ + AnimatedTextComponent.Item(id: self.isPlaying ? "random" : "selected", content: .text(self.isPlaying ? "Random" : "Selected")), + AnimatedTextComponent.Item(id: "traits", content: .text(" Traits")) + ], + noDelay: true, + blur: true + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((fillingSize - subtitleSize.width) * 0.5), y: contentHeight - 97.0), size: subtitleSize) + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.navigationBarContainer.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + + + let attributeSpacing: CGFloat = 10.0 + let attributeWidth: CGFloat = floor((fillingSize - sideInset * 2.0 - attributeSpacing * CGFloat(attributes.count - 1)) / CGFloat(attributes.count)) + let attributeHeight: CGFloat = 45.0 + + for i in 0 ..< attributes.count { + var attributeFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (attributeWidth + attributeSpacing), y: contentHeight - 60.0), size: CGSize(width: attributeWidth, height: attributeHeight)) + if i == attributes.count - 1 { + attributeFrame.size.width = max(0.0, availableSize.width - sideInset - attributeFrame.minX) + } + let attributeInfo: ComponentView + if self.attributeInfos.count > i { + attributeInfo = self.attributeInfos[i] + } else { + attributeInfo = ComponentView() + self.attributeInfos.append(attributeInfo) + } + let attribute = attributes[i] + let _ = attributeInfo.update( + transition: transition, + component: AnyComponent(AttributeInfoComponent( + strings: environment.strings, + backgroundColor: buttonColor, + secondaryTextColor: secondaryTextColor, + attribute: attribute + )), + environment: {}, + containerSize: attributeFrame.size + ) + if let attributeInfoView = attributeInfo.view { + if attributeInfoView.superview == nil { + self.navigationBarContainer.addSubview(attributeInfoView) + } + transition.setFrame(view: attributeInfoView, frame: attributeFrame) + } + } + + contentHeight += 16.0 + + let segmentedSize = self.segmentControl.update( + transition: transition, + component: AnyComponent(SegmentControlComponent( + theme: environment.theme, + items: [ + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.models), title: "Models"), + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.backdrops), title: "Backgrounds"), + SegmentControlComponent.Item(id: AnyHashable(SelectedSection.symbols), title: "Symbols") + ], + selectedId: "models", + action: { [weak self] id in + guard let self, let id = id.base as? SelectedSection else { + return + } + self.selectedSection = id + self.isPlaying = false + + self.updateEffectiveGifts() + self.state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))) //.withUserData(AnimationHint(value: .modeChanged))) + })), + environment: {}, + containerSize: CGSize(width: fillingSize - 8.0 * 2.0, height: 100.0) + ) + let segmentedControlFrame = CGRect(origin: CGPoint(x: floor((fillingSize - segmentedSize.width) * 0.5), y: contentHeight), size: segmentedSize) + if let segmentedControlComponentView = self.segmentControl.view { + if segmentedControlComponentView.superview == nil { + self.scrollContentView.addSubview(segmentedControlComponentView) + } + transition.setFrame(view: segmentedControlComponentView, frame: segmentedControlFrame) + } + contentHeight += segmentedSize.height + contentHeight += 18.0 + + + let descriptionText: String + switch self.selectedSection { + case .models: + descriptionText = "This collection features **\(self.modelCount)** unique models." + case .backdrops: + descriptionText = "This collection features **\(self.backdropCount)** unique backdrops." + case .symbols: + descriptionText = "This collection features **\(self.symbolCount)** unique symbols." + } + + let descriptionFont = Font.regular(13.0) + let descriptionBoldFont = Font.semibold(13.0) + let descriptionTextColor = theme.list.itemSecondaryTextColor + let descriptionMarkdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), bold: MarkdownAttributeSet(font: descriptionBoldFont, textColor: descriptionTextColor), link: MarkdownAttributeSet(font: descriptionFont, textColor: descriptionTextColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + + let descriptionSize = self.descriptionText.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: descriptionText, attributes: descriptionMarkdownAttributes) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let descriptionFrame = CGRect(origin: CGPoint(x: floor((fillingSize - descriptionSize.width) * 0.5), y: contentHeight), size: descriptionSize) + if let descriptionView = self.descriptionText.view { + if descriptionView.superview == nil { + self.scrollContentView.addSubview(descriptionView) + } + transition.setFrame(view: descriptionView, frame: descriptionFrame) + } + contentHeight += descriptionSize.height + contentHeight += 26.0 + + + + + //// + //// var validKeys: Set = Set() + //// for auctionState in self.auctionStates { + //// let id = auctionState.gift.giftId + //// validKeys.insert(id) + //// + //// let itemView: ComponentView + //// if let current = self.itemsViews[id] { + //// itemView = current + //// } else { + //// itemView = ComponentView() + //// self.itemsViews[id] = itemView + //// } + //// + //// let itemSize = itemView.update( + //// transition: transition, + //// component: AnyComponent( + //// ActiveAuctionComponent( + //// context: component.context, + //// theme: theme, + //// strings: environment.strings, + //// dateTimeFormat: environment.dateTimeFormat, + //// state: auctionState, + //// currentTime: currentTime, + //// action: { [weak self] in + //// guard let self, let component = self.component else { + //// return + //// } + //// if let giftAuctionsManager = component.context.giftAuctionsManager { + //// let _ = (giftAuctionsManager.auctionContext(for: .giftId(id)) + //// |> deliverOnMainQueue).start(next: { [weak self] auction in + //// guard let self, let component = self.component, let auction, let controller = environment.controller(), let navigationController = controller.navigationController as? NavigationController else { + //// return + //// } + //// let bidController = component.context.sharedContext.makeGiftAuctionBidScreen(context: component.context, toPeerId: auction.currentBidPeerId ?? component.context.account.peerId, text: nil, entities: nil, hideName: false, auctionContext: auction, acquiredGifts: nil) + //// navigationController.pushViewController(bidController) + //// }) + //// } + //// } + //// ) + //// ), + //// environment: {}, + //// containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + //// ) + //// let itemFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: itemSize) + //// if let view = itemView.view { + //// if view.superview == nil { + //// self.scrollContentView.addSubview(view) + //// } + //// view.frame = itemFrame + //// } + //// contentHeight += itemSize.height + //// contentHeight += 20.0 + //// } + //// contentHeight -= 10.0 + // + // var removeKeys: [Int64] = [] + // for (id, item) in self.itemsViews { + // if !validKeys.contains(id) { + // removeKeys.append(id) + // + // if let itemView = item.view { + // transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in + // itemView.removeFromSuperview() + // }) + // } + // } + // } + // for id in removeKeys { + // self.itemsViews.removeValue(forKey: id) + // } + + if self.backgroundHandleView.image == nil { + self.backgroundHandleView.image = generateStretchableFilledCircleImage(diameter: 5.0, color: .white)?.withRenderingMode(.alwaysTemplate) + } + self.backgroundHandleView.tintColor = UIColor.white.withAlphaComponent(0.4) + let backgroundHandleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - 36.0) * 0.5), y: 5.0), size: CGSize(width: 36.0, height: 5.0)) + if self.backgroundHandleView.superview == nil { + self.navigationBarContainer.addSubview(self.backgroundHandleView) + } + transition.setFrame(view: self.backgroundHandleView, frame: backgroundHandleFrame) + + self.glassContainerView.update(size: CGSize(width: fillingSize, height: 64.0), isDark: false, transition: .immediate) + self.glassContainerView.frame = CGRect(origin: CGPoint(x: rawSideInset, y: 0.0), size: CGSize(width: fillingSize, height: 64.0)) + + let closeButtonSize = self.closeButton.update( + transition: .immediate, + component: AnyComponent(GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: secondaryTextColor, + isDark: false, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Back", + tintColor: .white + ) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.environment?.controller()?.dismiss() + } + )), + environment: {}, + containerSize: CGSize(width: 40.0, height: 40.0) + ) + let closeButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: closeButtonSize) + if let closeButtonView = self.closeButton.view { + if closeButtonView.superview == nil { + self.navigationBarContainer.addSubview(self.glassContainerView) + self.glassContainerView.contentView.addSubview(closeButtonView) + } + transition.setFrame(view: closeButtonView, frame: closeButtonFrame) + } + + var playbackItems: [AnyComponentWithIdentity] = [] + var playbackSize = CGSize(width: 40.0, height: 40.0) + if self.isPlaying { + playbackItems.append(AnyComponentWithIdentity(id: "pause", component: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PictureInPicturePause", + tintColor: .white + ) + ))) + } else { + if self.showRandomizeTip { + playbackItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: "Randomize Traits", font: Font.semibold(17.0), textColor: .white))) + ))) + playbackSize.width = 186.0 + } + playbackItems.append(AnyComponentWithIdentity(id: "play", component: AnyComponent( + BundleIconComponent( + name: "Media Gallery/PlayButton", + tintColor: .white + ) + ))) + } + + let playbackButtonSize = self.playbackButton.update( + transition: transition, + component: AnyComponent(GlassBarButtonComponent( + size: playbackSize, + backgroundColor: secondaryTextColor, + isDark: false, + state: .tintedGlass, + component: AnyComponentWithIdentity(id: "content", component: AnyComponent( + HStack(playbackItems, spacing: 1.0) + )), + action: { [weak self] _ in + guard let self else { + return + } + self.isPlaying = !self.isPlaying + + if !self.isPlaying { + self.showRandomizeTip = true + Queue.mainQueue().after(2.0) { + if self.showRandomizeTip { + self.showRandomizeTip = false + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + } + } else { + self.selectedModel = nil + self.selectedBackdrop = nil + self.selectedSymbol = nil + + self.showRandomizeTip = false + + self.timerTick() + } + self.state?.updated(transition: .easeInOut(duration: 0.25)) + } + )), + environment: {}, + containerSize: playbackSize + ) + let playbackButtonFrame = CGRect(origin: CGPoint(x: fillingSize - 16.0 - playbackButtonSize.width, y: 16.0), size: playbackButtonSize) + if let playbackButtonView = self.playbackButton.view { + if playbackButtonView.superview == nil { + playbackButtonView.clipsToBounds = true + self.glassContainerView.contentView.addSubview(playbackButtonView) + } + transition.setFrame(view: playbackButtonView, frame: playbackButtonFrame) + } + + //TODO:release + contentHeight += 126.0 * 17.0 + + let containerInset: CGFloat = environment.statusBarHeight + 10.0 + contentHeight += environment.safeInsets.bottom + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + initialContentHeight = contentHeight + + clippingY = availableSize.height + + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) + + let scrollContentHeight = max(topInset + contentHeight + containerInset, availableSize.height - containerInset) + + self.scrollContentClippingView.layer.cornerRadius = 38.0 + + self.itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, containerCornerRadius: environment.deviceMetrics.screenCornerRadius, bottomInset: environment.safeInsets.bottom, topInset: topInset) + + transition.setFrame(view: self.scrollContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + containerInset), size: CGSize(width: availableSize.width, height: contentHeight))) + + transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) + transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: CGSize(width: fillingSize, height: availableSize.height))) + + let scrollClippingFrame = CGRect(origin: CGPoint(x: 0.0, y: containerInset), size: CGSize(width: availableSize.width, height: clippingY - containerInset)) + transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) + transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) + let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) + if contentSize != self.scrollView.contentSize { + self.scrollView.contentSize = contentSize + } + if resetScrolling { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize) + } + self.ignoreScrolling = false + self.updateScrolling(transition: transition) + + transition.setPosition(view: self.containerView, position: CGRect(origin: CGPoint(), size: availableSize).center) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + + if let controller = environment.controller(), !controller.automaticallyControlPresentationContextLayout { + let bottomInset: CGFloat = contentHeight - 12.0 + + let layout = ContainerViewLayout( + size: availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: transition.containedViewLayoutTransition) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public class GiftUpgradePreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + + private var didPlayAppearAnimation: Bool = false + private var isDismissed: Bool = false + + public init(context: AccountContext) { + self.context = context + + super.init(context: context, component: GiftUpgradePreviewScreenComponent( + context: context + ), navigationBarAppearance: .none, theme: .default) + + self.statusBar.statusBarStyle = .Ignore + self.navigationPresentation = .flatModal + self.blocksBackgroundWhenInOverlay = true + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.view.disablesInteractiveModalDismiss = true + + if !self.didPlayAppearAnimation { + self.didPlayAppearAnimation = true + + if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View { + componentView.animateIn() + } + } + } + + override public func dismiss(completion: (() -> Void)? = nil) { + if !self.isDismissed { + self.isDismissed = true + + if let componentView = self.node.hostView.componentView as? GiftUpgradePreviewScreenComponent.View { + componentView.animateOut(completion: { [weak self] in + completion?() + self?.dismiss(animated: false) + }) + } else { + self.dismiss(animated: false) + } + } + } +} + +private final class AttributeInfoComponent: Component { + let strings: PresentationStrings + let backgroundColor: UIColor + let secondaryTextColor: UIColor + let attribute: StarGift.UniqueGift.Attribute + + init( + strings: PresentationStrings, + backgroundColor: UIColor, + secondaryTextColor: UIColor, + attribute: StarGift.UniqueGift.Attribute + ) { + self.strings = strings + self.backgroundColor = backgroundColor + self.secondaryTextColor = secondaryTextColor + self.attribute = attribute + } + + static func ==(lhs: AttributeInfoComponent, rhs: AttributeInfoComponent) -> Bool { + if lhs.strings !== rhs.strings { + return false + } + if lhs.backgroundColor != rhs.backgroundColor { + return false + } + if lhs.secondaryTextColor != rhs.secondaryTextColor { + return false + } + if lhs.attribute != rhs.attribute { + return false + } + return true + } + + final class View: UIView { + let background = SimpleLayer() + let title = ComponentView() + let subtitle = ComponentView() + + let badgeBackground = SimpleLayer() + let badge = ComponentView() + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: AttributeInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let backgroundFrame = CGRect(origin: CGPoint(), size: availableSize) + if self.background.superlayer == nil { + self.background.cornerRadius = 16.0 + self.background.cornerCurve = .continuous + self.layer.addSublayer(self.background) + + self.badgeBackground.cornerRadius = 9.5 + self.badgeBackground.cornerCurve = .continuous + self.layer.addSublayer(self.badgeBackground) + } + self.background.frame = backgroundFrame + transition.setBackgroundColor(layer: self.background, color: component.backgroundColor) + + let title: String + let subtitle: String + let rarity: Int32 + switch component.attribute { + case let .model(name, _, rarityValue): + title = name + subtitle = "model" + rarity = rarityValue + case let .backdrop(name, _, _, _, _, _, rarityValue): + title = name + subtitle = "backdrop" + rarity = rarityValue + case let .pattern(name, _, rarityValue): + title = name + subtitle = "symbol" + rarity = rarityValue + default: + title = "" + subtitle = "" + rarity = 0 + } + + let _ = rarity + + let titleSize = self.title.update( + transition: .spring(duration: 0.2), + component: AnyComponent(AnimatedTextComponent( + font: Font.semibold(13.0), + color: UIColor.white, + items: [AnimatedTextComponent.Item(id: "title", content: .text(title))], + noDelay: true, + blur: true + )), + environment: {}, + containerSize: CGSize(width: backgroundFrame.size.width - 8.0, height: backgroundFrame.size.height) + ) + let subtitleSize = self.subtitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: subtitle, font: Font.regular(11.0), textColor: .white)), + tintColor: component.secondaryTextColor + )), + environment: {}, + containerSize: backgroundFrame.size + ) + + let spacing: CGFloat = 0.0 + let titleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - titleSize.width) * 0.5), y: floor((backgroundFrame.height - titleSize.height - spacing - subtitleSize.height) * 0.5)), size: titleSize) + let subtitleFrame = CGRect(origin: CGPoint(x: floor((backgroundFrame.width - subtitleSize.width) * 0.5), y: titleFrame.maxY + spacing), size: subtitleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + if let subtitleView = self.subtitle.view { + if subtitleView.superview == nil { + self.addSubview(subtitleView) + } + transition.setFrame(view: subtitleView, frame: subtitleFrame) + } + + func formatPercentage(_ value: Float) -> String { + return String(format: "%0.1f", value).replacingOccurrences(of: ".0", with: "").replacingOccurrences(of: ",0", with: "") + } + let percentage = Float(rarity) * 0.1 + + let badgeSize = self.badge.update( + transition: .spring(duration: 0.2), + component: AnyComponent(AnimatedTextComponent( + font: Font.with(size: 12.0, weight: .semibold, traits: .monospacedNumbers), + color: UIColor.white, + items: [ + AnimatedTextComponent.Item(id: "value", content: .text(formatPercentage(percentage))), + AnimatedTextComponent.Item(id: "percent", content: .text("%")), + ], + noDelay: true, + blur: true + )), + environment: {}, + containerSize: backgroundFrame.size + ) + let badgeFrame = CGRect(origin: CGPoint(x: backgroundFrame.width - badgeSize.width - 2.0, y: backgroundFrame.minY - 8.0), size: badgeSize) + if let badgeView = self.badge.view { + if badgeView.superview == nil { + self.addSubview(badgeView) + } + transition.setFrame(view: badgeView, frame: badgeFrame) + } + + let badgeBackgroundFrame = badgeFrame.insetBy(dx: -5.5, dy: -2.0) + transition.setFrame(layer: self.badgeBackground, frame: badgeBackgroundFrame) + transition.setBackgroundColor(layer: self.badgeBackground, color: component.backgroundColor.mixedWith(component.secondaryTextColor, alpha: 0.5)) + + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift index 2067859721..5586711858 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftValueScreen.swift @@ -370,7 +370,7 @@ private final class GiftValueSheetContent: CombinedComponent { ) )) - let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (≈\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))" + let valueString = "⭐️\(formatStarsAmountText(StarsAmount(value: component.valueInfo.initialSaleStars, nanos: 0), dateTimeFormat: dateTimeFormat)) (~\(formatCurrencyAmount(component.valueInfo.initialSalePrice, currency: component.valueInfo.currency)))" let valueAttributedString = NSMutableAttributedString(string: valueString, font: tableFont, textColor: tableTextColor) let range = (valueAttributedString.string as NSString).range(of: "⭐️") if range.location != NSNotFound { diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index ccb673df45..c0ad559a13 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1438,7 +1438,7 @@ private final class GiftViewSheetContent: CombinedComponent { }))) //TODO:localize - if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId { + if case let .unique(uniqueGift) = arguments.gift, case let .peerId(ownerPeerId) = uniqueGift.owner, ownerPeerId != self.context.account.peerId, uniqueGift.minOfferStars != nil { items.append(.action(ContextMenuActionItem(text: "Offer to Buy", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Paid"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in @@ -4207,7 +4207,7 @@ private final class GiftViewSheetContent: CombinedComponent { HStack([ AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "≈\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "~\(formatCurrencyAmount(valueAmount, currency: valueCurrency))", font: tableFont, textColor: tableTextColor)))) ), AnyComponentWithIdentity( id: AnyHashable(1), @@ -5382,6 +5382,12 @@ public class GiftViewScreen: ViewControllerComponentContainer { resellAmounts = uniqueGift.resellAmounts } return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars, nil) + case let .starGiftPurchaseOffer(gift, _, _, _, _), let .starGiftPurchaseOfferDeclined(gift, _, _): + if case let .unique(gift) = gift { + return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil) + } else { + return nil + } default: return nil } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift index eb9e874dd7..8d128e4a0a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/TableComponent.swift @@ -53,10 +53,12 @@ final class TableComponent: CombinedComponent { private let theme: PresentationTheme private let items: [Item] - - public init(theme: PresentationTheme, items: [Item]) { + private let semiTransparent: Bool + + public init(theme: PresentationTheme, items: [Item], semiTransparent: Bool = false) { self.theme = theme self.items = items + self.semiTransparent = semiTransparent } public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { @@ -66,6 +68,9 @@ final class TableComponent: CombinedComponent { if lhs.items != rhs.items { return false } + if lhs.semiTransparent != rhs.semiTransparent { + return false + } return true } @@ -95,7 +100,10 @@ final class TableComponent: CombinedComponent { let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) - let secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor + var secondaryBackgroundColor = context.component.theme.overallDarkAppearance ? context.component.theme.list.itemModalBlocksBackgroundColor : context.component.theme.list.itemInputField.backgroundColor + if context.component.semiTransparent { + secondaryBackgroundColor = borderColor.withMultipliedAlpha(0.5) + } var leftColumnWidth: CGFloat = 0.0 diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 814fcb4edd..448ecd6c1b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -68,6 +68,8 @@ swift_library( "//submodules/TelegramUI/Components/MediaAssetsContext", "//submodules/CheckNode", "//submodules/TelegramNotices", + "//submodules/TelegramUI/Components/AttachmentFileController", + "//submodules/SaveToCameraRoll", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 71d2a6fedd..1b649ed216 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -50,6 +50,8 @@ import UIKitRuntimeUtils import ImageObjectSeparation import SaveProgressScreen import TelegramNotices +import AttachmentFileController +import SaveToCameraRoll private let playbackButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() @@ -3732,7 +3734,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } else if case let .gift(gift) = subject { isGift = true - let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false))] + let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, isPrepaidUpgrade: false, peerId: nil, senderId: nil, savedId: nil, resaleAmount: nil, canTransferDate: nil, canResaleDate: nil, dropOriginalDetailsStars: nil, assigned: false, fromOffer: false))] let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) messages = .single([message]) } else { @@ -5107,8 +5109,65 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } controller.push(locationController) } - + func presentAudioPicker() { + guard let controller = self.controller else { + return + } + + let audioController = storyAudioPickerController( + context: self.context, + selectFromFiles: { [weak self] in + self?.presentAudioFilePicker() + }, + dismissed: { [weak self] in + if let self { + self.mediaEditor?.play() + } + }, + completion: { [weak self] file in + guard let self else { + return + } + let _ = (fetchMediaData( + context: self.context, + postbox: self.context.account.postbox, + userLocation: .other, + mediaReference: file + ) |> deliverOnMainQueue).start(next: { [weak self] state, _ in + guard let self else { + return + } + if case let .data(data) = state { + let path = data.path + + try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true) + + var originalFileName: String = "audio_\(Int.random(in: 0 ..< .max)).mp3" + if let file = file.media as? TelegramMediaFile { + originalFileName = file.fileName ?? "\(file.fileId.id).mp3" + } + + let fileName = "audio_\(originalFileName)" + let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName) + + try? FileManager.default.removeItem(atPath: copyPath) + do { + try FileManager.default.copyItem(atPath: path, toPath: copyPath) + } catch let e { + Logger.shared.log("MediaEditor", "copy file error \(e)") + return + } + + self.insertAudio(path: copyPath, fileName: fileName) + } + }) + } + ) + controller.push(audioController) + } + + func presentAudioFilePicker() { var isSettingTrack = false self.controller?.present(legacyICloudFilePicker(theme: self.presentationData.theme, mode: .import, documentTypes: ["public.mp3", "public.mpeg-4-audio", "public.aac-audio", "org.xiph.flac"], forceDarkTheme: true, dismissed: { [weak self] in if let self { @@ -5119,7 +5178,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } } }, completion: { [weak self] urls in - guard let self, let mediaEditor = self.mediaEditor, !urls.isEmpty, let url = urls.first else { + guard let self, !urls.isEmpty, let url = urls.first else { return } isSettingTrack = true @@ -5132,7 +5191,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID let coordinator = NSFileCoordinator(filePresenter: nil) var error: NSError? coordinator.coordinate(readingItemAt: url, options: .forUploading, error: &error, byAccessor: { sourceUrl in - let fileName = "audio_\(sourceUrl.lastPathComponent)" + let fileName = "audio_\(sourceUrl.lastPathComponent)" let copyPath = fullDraftPath(peerId: self.context.account.peerId, path: fileName) try? FileManager.default.removeItem(atPath: copyPath) @@ -5147,92 +5206,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } Queue.mainQueue().async { - let audioAsset = AVURLAsset(url: URL(fileURLWithPath: copyPath)) - - func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) { - asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: { - if asset.statusOfValue(forKey: "tracks", error: nil) == .loading { - if retryCount < 2 { - Queue.mainQueue().after(0.1, { - loadValues(asset: asset, retryCount: retryCount + 1, completion: completion) - }) - } else { - completion() - } - } else { - completion() - } - }) - } - - loadValues(asset: audioAsset, retryCount: 0, completion: { - var audioDuration: Double = 0.0 - guard let track = audioAsset.tracks(withMediaType: .audio).first else { - Logger.shared.log("MediaEditor", "track is nil") - if isScopedResource { - url.stopAccessingSecurityScopedResource() - } - return - } - - audioDuration = track.timeRange.duration.seconds - if audioDuration.isZero { - Logger.shared.log("MediaEditor", "duration is zero") - if isScopedResource { - url.stopAccessingSecurityScopedResource() - } - return - } - - func maybeFixMisencodedText(_ text: String) -> String { - let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ") - if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil { - if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) { - return string - } else { - return text - } - } else { - return text - } - } - - var artist: String? - var title: String? - for data in audioAsset.commonMetadata { - if data.commonKey == .commonKeyArtist, let value = data.stringValue { - artist = maybeFixMisencodedText(value) - } - if data.commonKey == .commonKeyTitle, let value = data.stringValue { - title = maybeFixMisencodedText(value) - } - } - - Queue.mainQueue().async { - var audioTrimRange: Range? - var audioOffset: Double? - - if let videoDuration = mediaEditor.originalCappedDuration { - if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound { - audioOffset = -videoStart - } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound { - audioOffset = -videoStart - } - audioTrimRange = 0 ..< min(videoDuration, audioDuration) - } else { - audioTrimRange = 0 ..< min(15, audioDuration) - } - - mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset) - - mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true) - - self.requestUpdate(transition: .easeInOut(duration: 0.2)) - if isScopedResource { - url.stopAccessingSecurityScopedResource() - } - - mediaEditor.play() + self.insertAudio(path: copyPath, fileName: fileName, dispose: { + if isScopedResource { + url.stopAccessingSecurityScopedResource() } }) } @@ -5244,6 +5220,94 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }), in: .window(.root)) } + private func insertAudio(path: String, fileName: String, dispose: (() -> Void)? = nil) { + guard let mediaEditor = self.mediaEditor else { + return + } + let audioAsset = AVURLAsset(url: URL(fileURLWithPath: path)) + + func loadValues(asset: AVAsset, retryCount: Int, completion: @escaping () -> Void) { + asset.loadValuesAsynchronously(forKeys: ["tracks", "duration"], completionHandler: { + if asset.statusOfValue(forKey: "tracks", error: nil) == .loading { + if retryCount < 2 { + Queue.mainQueue().after(0.1, { + loadValues(asset: asset, retryCount: retryCount + 1, completion: completion) + }) + } else { + completion() + } + } else { + completion() + } + }) + } + + loadValues(asset: audioAsset, retryCount: 0, completion: { + var audioDuration: Double = 0.0 + guard let track = audioAsset.tracks(withMediaType: .audio).first else { + Logger.shared.log("MediaEditor", "track is nil") + dispose?() + return + } + + audioDuration = track.timeRange.duration.seconds + if audioDuration.isZero { + Logger.shared.log("MediaEditor", "duration is zero") + dispose?() + return + } + + func maybeFixMisencodedText(_ text: String) -> String { + let charactersToSearchFor = CharacterSet(charactersIn: "àåèîóûþÿ") + if text.lowercased().rangeOfCharacter(from: charactersToSearchFor) != nil { + if let data = text.data(using: .windowsCP1252), let string = String(data: data, encoding: .windowsCP1251) { + return string + } else { + return text + } + } else { + return text + } + } + + var artist: String? + var title: String? + for data in audioAsset.commonMetadata { + if data.commonKey == .commonKeyArtist, let value = data.stringValue { + artist = maybeFixMisencodedText(value) + } + if data.commonKey == .commonKeyTitle, let value = data.stringValue { + title = maybeFixMisencodedText(value) + } + } + + Queue.mainQueue().async { + var audioTrimRange: Range? + var audioOffset: Double? + + if let videoDuration = mediaEditor.originalCappedDuration { + if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound { + audioOffset = -videoStart + } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound { + audioOffset = -videoStart + } + audioTrimRange = 0 ..< min(videoDuration, audioDuration) + } else { + audioTrimRange = 0 ..< min(15, audioDuration) + } + + mediaEditor.setAudioTrack(MediaAudioTrack(path: fileName, artist: artist, title: title, duration: audioDuration), trimRange: audioTrimRange, offset: audioOffset) + + mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: true) + + self.requestUpdate(transition: .easeInOut(duration: 0.2)) + dispose?() + + mediaEditor.play() + } + }) + } + func presentVideoRemoveConfirmation() { guard let controller = self.controller else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift index 2ee1e33879..be77701a7b 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PostSuggestionsSettingsScreen/Sources/PostSuggestionsSettingsScreen.swift @@ -348,7 +348,7 @@ final class PostSuggestionsSettingsScreenComponent: Component { var contentSectionItems: [AnyComponentWithIdentity] = [] let usdRate = Double(component.usdWithdrawRate) / 1000.0 / 100.0 - let price = self.starCount == 0 ? "" : "≈\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" + let price = self.starCount == 0 ? "" : "~\(formatTonUsdValue(Int64(self.starCount), divide: false, rate: usdRate, dateTimeFormat: presentationData.dateTimeFormat))" contentSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListItemComponentAdaptor( itemGenerator: MessagePriceItem( diff --git a/submodules/TelegramUI/Components/SegmentControlComponent/BUILD b/submodules/TelegramUI/Components/SegmentControlComponent/BUILD new file mode 100644 index 0000000000..b4d37a733c --- /dev/null +++ b/submodules/TelegramUI/Components/SegmentControlComponent/BUILD @@ -0,0 +1,23 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "SegmentControlComponent", + module_name = "SegmentControlComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters", + "//submodules/SegmentedControlNode", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift similarity index 92% rename from submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift rename to submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift index 4f2cb4878b..69d6cb6853 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/SegmentControlComponent.swift +++ b/submodules/TelegramUI/Components/SegmentControlComponent/Sources/SegmentControlComponent.swift @@ -7,10 +7,15 @@ import ComponentDisplayAdapters import TelegramPresentationData import SegmentedControlNode -final class SegmentControlComponent: Component { - struct Item: Equatable { +public final class SegmentControlComponent: Component { + public struct Item: Equatable { var id: AnyHashable var title: String + + public init(id: AnyHashable, title: String) { + self.id = id + self.title = title + } } let theme: PresentationTheme @@ -18,7 +23,7 @@ final class SegmentControlComponent: Component { let selectedId: AnyHashable? let action: (AnyHashable) -> Void - init( + public init( theme: PresentationTheme, items: [Item], selectedId: AnyHashable?, @@ -30,7 +35,7 @@ final class SegmentControlComponent: Component { self.action = action } - static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool { + public static func ==(lhs: SegmentControlComponent, rhs: SegmentControlComponent) -> Bool { if lhs.theme !== rhs.theme { return false } @@ -80,7 +85,7 @@ final class SegmentControlComponent: Component { } } - class View: UIView { + public class View: UIView { private let title = ComponentView() private var component: SegmentControlComponent? @@ -88,7 +93,7 @@ final class SegmentControlComponent: Component { private var nativeSegmentedView: SegmentedControlView? private var legacySegmentedNode: SegmentedControlNode? - override init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) } @@ -171,11 +176,11 @@ final class SegmentControlComponent: Component { } } - func makeView() -> View { + public func makeView() -> View { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index af4d150393..c3e484a85e 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -220,7 +220,7 @@ final class StarsBalanceComponent: Component { let subtitleText: String if let rate = component.rate { - subtitleText = "≈\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))" + subtitleText = "~\(formatTonUsdValue(component.count.value, divide: false, rate: rate, dateTimeFormat: component.dateTimeFormat))" } else { subtitleText = component.strings.Stars_Intro_YourBalance } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift index 3551b536e3..9130474ac6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsOverviewItemComponent.swift @@ -114,7 +114,7 @@ final class StarsOverviewItemComponent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "≈\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + text: .plain(NSAttributedString(string: "~\(usdValueString)", font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) ) ), environment: {}, diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD index 573462e416..4e7000f275 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/BUILD @@ -41,6 +41,7 @@ swift_library( "//submodules/TelegramUI/Components/TabSelectorComponent", "//submodules/TelegramUI/Components/Stars/BalanceNeededScreen", "//submodules/TelegramUI/Components/GlassBarButtonComponent", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift index a1ebcb086f..aaa02178ac 100644 --- a/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsWithdrawalScreen/Sources/StarsWithdrawalScreen.swift @@ -27,6 +27,7 @@ import TabSelectorComponent import PresentationDataUtils import BalanceNeededScreen import GlassBarButtonComponent +import GlassBackgroundComponent private let amountTag = GenericComponentViewTag() @@ -68,6 +69,7 @@ private final class SheetContent: CombinedComponent { let balanceTitle = Child(MultilineTextComponent.self) let balanceValue = Child(MultilineTextComponent.self) let balanceIcon = Child(BundleIconComponent.self) + let durationPicker = Child(MenuComponent.self) let body: (CombinedComponentContext) -> CGSize = { (context: CombinedComponentContext) -> CGSize in let environment = context.environment[EnvironmentType.self] @@ -356,6 +358,7 @@ private final class SheetContent: CombinedComponent { tonTitle = environment.strings.Chat_PostSuggestion_Suggest_RequestTon } case .starGiftOffer: + //TODO:localize displayCurrencySelector = true starsTitle = "Offer Stars" tonTitle = "Offer TON" @@ -625,6 +628,7 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width - amountAdditionalLabel.size.width / 2.0 - sideInset - 16.0, y: contentSize.height - amountAdditionalLabel.size.height / 2.0))) } + var durationFrame = CGRect() if case .starGiftResell = component.mode { contentSize.height += 24.0 @@ -808,6 +812,14 @@ private final class SheetContent: CombinedComponent { maximumNumberOfLines: 0 )) + let hours = state.duration / 3600 + let durationString: String + if hours == 1 { + durationString = "1 Hour" + } else { + durationString = "\(hours) Hours" + } + let periodSection = periodSection.update( component: ListSectionComponent( theme: theme, @@ -831,15 +843,19 @@ private final class SheetContent: CombinedComponent { ], alignment: .left, spacing: 2.0)), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "48 Hours", + string: durationString, font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 1 )))), accessory: .expandArrows, - action: { _ in - + action: { [weak state] _ in + guard let state else { + return + } + state.isPickingDuration = true + state.updated(transition: .easeInOut(duration: 0.25)) } )) )] @@ -853,6 +869,8 @@ private final class SheetContent: CombinedComponent { .clipsToBounds(true) .cornerRadius(10.0) ) + durationFrame = CGRect(origin: CGPoint(x: context.availableSize.width / 2.0 - periodSection.size.width / 2.0, y: contentSize.height), size: periodSection.size) + contentSize.height += periodSection.size.height } @@ -1065,6 +1083,58 @@ private final class SheetContent: CombinedComponent { contentSize.height += buttonInsets.bottom } + + if state.isPickingDuration { + let durationPicker = durationPicker.update( + component: MenuComponent( + theme: theme, + sourceFrame: durationFrame.offsetBy(dx: 0.0, dy: 120.0), + component: AnyComponent(DurationMenuComponent( + theme: theme, + strings: environment.strings, + value: state.duration, + valueUpdated: { [weak state] value in + guard let state else { + return + } + state.isPickingDuration = false + state.duration = value + + state.updated(transition: .easeInOut(duration: 0.25)) + } + )), + dismiss: { [weak state] in + guard let state else { + return + } + state.isPickingDuration = false + state.updated(transition: .easeInOut(duration: 0.25)) + } + ), + availableSize: contentSize, + transition: context.transition + ) + context.add(durationPicker + .position(CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0)) + .appear(ComponentTransition.Appear({ _, view, transition in + if !transition.animation.isImmediate { + if let view = view as? MenuComponent.View { + view.animateIn() + } + } + })) + .disappear(ComponentTransition.Disappear({ view, transition, completion in + if !transition.animation.isImmediate { + if let view = view as? MenuComponent.View { + view.animateOut(completion: completion) + } + } else { + completion() + } + })) + ) + } + return contentSize } @@ -1080,7 +1150,7 @@ private final class SheetContent: CombinedComponent { fileprivate var amount: StarsAmount? fileprivate var currency: CurrencyAmount.Currency = .stars fileprivate var timestamp: Int32? - fileprivate var duration: Int32 = 120 + fileprivate var duration: Int32 = 172800 fileprivate var starsBalance: StarsAmount? private var starsStateDisposable: Disposable? @@ -1091,6 +1161,8 @@ private final class SheetContent: CombinedComponent { var cachedTonImage: (UIImage, PresentationTheme)? var cachedChevronImage: (UIImage, PresentationTheme)? + var isPickingDuration = false + init(component: SheetContent) { self.context = component.context self.mode = component.mode @@ -2251,3 +2323,423 @@ private final class CurrencyTabItemComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + + +private final class MenuComponent: Component { + let theme: PresentationTheme + let sourceFrame: CGRect + let component: AnyComponent + let dismiss: () -> Void + + init( + theme: PresentationTheme, + sourceFrame: CGRect, + component: AnyComponent, + dismiss: @escaping () -> Void + ) { + self.theme = theme + self.sourceFrame = sourceFrame + self.component = component + self.dismiss = dismiss + } + + public static func ==(lhs: MenuComponent, rhs: MenuComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.sourceFrame != rhs.sourceFrame { + return false + } + if lhs.component != rhs.component { + return false + } + return true + } + + public final class View: UIView { + private let buttonView: UIButton + private let containerView: GlassBackgroundContainerView + private let backgroundView: GlassBackgroundView + private var componentView: ComponentView? + + private var component: MenuComponent? + + public override init(frame: CGRect) { + self.buttonView = UIButton() + self.containerView = GlassBackgroundContainerView() + self.backgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.buttonView) + self.addSubview(self.containerView) + self.containerView.contentView.addSubview(self.backgroundView) + + self.buttonView.addTarget(self, action: #selector(self.tapped), for: .touchUpInside) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func tapped() { + if let component = self.component { + component.dismiss() + } + } + + func animateIn() { + guard let component = self.component else { + return + } + let transition = ComponentTransition.spring(duration: 0.3) + transition.animatePosition(view: self.backgroundView, from: component.sourceFrame.center, to: self.backgroundView.center) + transition.animateScale(view: self.backgroundView, from: 0.2, to: 1.0) + self.containerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + } + + public func animateOut(completion: (() -> Void)? = nil) { + guard let component = self.component else { + return + } + + let transition = ComponentTransition.spring(duration: 0.3) + transition.setPosition(view: self.backgroundView, position: component.sourceFrame.center) + transition.setScale(view: self.backgroundView, scale: 0.2) + self.containerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in + completion?() + }) + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if !self.backgroundView.frame.contains(point) && self.buttonView.frame.contains(point) { + return self.buttonView + } + return super.hitTest(point, with: event) + } + + func update(component: MenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + var componentView: ComponentView + var componentTransition = transition + if let current = self.componentView { + componentView = current + } else { + componentTransition = .immediate + componentView = ComponentView() + self.componentView = componentView + } + + let componentSize = componentView.update( + transition: componentTransition, + component: component.component, + environment: {}, + containerSize: availableSize + ) + let backgroundFrame = CGRect(origin: CGPoint(x: component.sourceFrame.maxX - componentSize.width, y: component.sourceFrame.minY - componentSize.height - 20.0), size: componentSize) + if let view = componentView.view { + if view.superview == nil { + self.backgroundView.contentView.addSubview(view) + } + componentTransition.setFrame(view: view, frame: CGRect(origin: .zero, size: componentSize)) + } + + self.backgroundView.update(size: backgroundFrame.size, cornerRadius: 30.0, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: component.theme.chat.inputPanel.inputBackgroundColor.withMultipliedAlpha(0.7)), transition: transition) + self.backgroundView.frame = backgroundFrame + + self.containerView.frame = CGRect(origin: .zero, size: availableSize) + self.containerView.update(size: availableSize, isDark: component.theme.overallDarkAppearance, transition: transition) + + self.buttonView.frame = CGRect(origin: .zero, size: availableSize) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class MenuButtonComponent: Component { + let theme: PresentationTheme + let text: String + let isSelected: Bool + let width: CGFloat? + let action: () -> Void + + init( + theme: PresentationTheme, + text: String, + isSelected: Bool, + width: CGFloat?, + action: @escaping () -> Void + ) { + self.theme = theme + self.text = text + self.isSelected = isSelected + self.width = width + self.action = action + } + + static func ==(lhs: MenuButtonComponent, rhs: MenuButtonComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.isSelected != rhs.isSelected { + return false + } + if lhs.width != rhs.width { + return false + } + return true + } + + final class View: UIView { + private var component: MenuButtonComponent? + private weak var componentState: EmptyComponentState? + + private let selectionLayer = SimpleLayer() + private let title = ComponentView() + private let icon = ComponentView() + private let button = HighlightTrackingButton() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.layer.addSublayer(self.selectionLayer) + self.selectionLayer.masksToBounds = true + self.selectionLayer.opacity = 0.0 + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + + self.button.highligthedChanged = { [weak self] highlighted in + if let self { + if highlighted { + self.selectionLayer.opacity = 1.0 + self.selectionLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } else { + self.selectionLayer.opacity = 0.0 + self.selectionLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed() { + if let component = self.component { + component.action() + } + } + + func update(component: MenuButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + self.componentState = state + + let leftInset: CGFloat = 60.0 + let rightInset: CGFloat = 40.0 + + let titleSize = self.title.update( + transition: transition, + component: AnyComponent( + Text(text: component.text, font: Font.regular(17.0), color: component.theme.contextMenu.primaryColor) + ), + environment: {}, + containerSize: availableSize + ) + let titleFrame = CGRect(origin: CGPoint(x: 60.0, y: floorToScreenPixels((availableSize.height - titleSize.height) / 2.0)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + titleView.frame = titleFrame + } + + let size = CGSize(width: component.width ?? (leftInset + rightInset + titleSize.width), height: availableSize.height) + + if component.isSelected { + let iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent( + name: "Media Gallery/Check", + tintColor: component.theme.contextMenu.primaryColor + ) + ), + environment: {}, + containerSize: CGSize(width: 44.0, height: 44.0) + ) + let iconFrame = CGRect(origin: CGPoint(x: 25.0, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize) + if let iconView = self.icon.view { + if iconView.superview == nil { + self.addSubview(iconView) + } + iconView.frame = iconFrame + } + } + + self.selectionLayer.backgroundColor = component.theme.contextMenu.itemHighlightedBackgroundColor.withMultipliedAlpha(0.5).cgColor + transition.setFrame(layer: self.selectionLayer, frame: CGRect(origin: .zero, size: size).insetBy(dx: 10.0, dy: 0.0)) + self.selectionLayer.cornerRadius = size.height / 2.0 + + if self.button.superview == nil { + self.addSubview(self.button) + } + self.button.frame = CGRect(origin: .zero, size: size) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +private final class DurationMenuComponent: Component { + let theme: PresentationTheme + let strings: PresentationStrings + let value: Int32 + let valueUpdated: (Int32) -> Void + + init( + theme: PresentationTheme, + strings: PresentationStrings, + value: Int32, + valueUpdated: @escaping (Int32) -> Void + ) { + self.theme = theme + self.strings = strings + self.value = value + self.valueUpdated = valueUpdated + } + + public static func ==(lhs: DurationMenuComponent, rhs: DurationMenuComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public final class View: UIView { + private let backgroundView: GlassBackgroundView + private var itemViews: [Int32: ComponentView] = [:] + + private var component: DurationMenuComponent? + + private let values: [Int32] = [ + 21600, 43200, 86400, 129600, 172800, 259200 + ] + + private var width: CGFloat? + + public override init(frame: CGRect) { + self.backgroundView = GlassBackgroundView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: DurationMenuComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let itemHeight: CGFloat = 40.0 + + var maxWidth: CGFloat = 0.0 + var originY: CGFloat = 12.0 + for value in self.values { + let itemView: ComponentView + if let current = self.itemViews[value] { + itemView = current + } else { + itemView = ComponentView() + self.itemViews[value] = itemView + } + + let repeatString: String + let hours = value / 3600 + //TODO:localize + if hours == 1 { + repeatString = "1 hour" + } else { + repeatString = "\(hours) hours" + } + + let itemSize = itemView.update( + transition: transition, + component: AnyComponent( + MenuButtonComponent( + theme: component.theme, + text: repeatString, + isSelected: component.value == value, + width: self.width, + action: { [weak self] in + guard let self else { + return + } + self.component?.valueUpdated(value) + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: itemHeight) + ) + maxWidth = max(maxWidth, itemSize.width) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: originY), size: itemSize) + if let itemView = itemView.view { + if itemView.superview == nil { + self.addSubview(itemView) + } + transition.setFrame(view: itemView, frame: itemFrame) + } + originY += 40.0 + } + + let size = CGSize(width: maxWidth, height: originY + 8.0) + + if self.width == nil { + self.width = maxWidth + Queue.mainQueue().justDispatch { + state.updated() + } + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD index dc32b21d4f..096451d9b2 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/BUILD +++ b/submodules/TelegramUI/Components/StorageUsageScreen/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/BottomButtonPanelComponent", "//submodules/TelegramUI/Components/SliderComponent", + "//submodules/TelegramUI/Components/SegmentControlComponent", "//submodules/CheckNode", "//submodules/Markdown", "//submodules/ContextUI", diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift index 4ceb81cb3c..bac4438def 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/DataUsageScreen.swift @@ -23,6 +23,7 @@ import TelegramStringFormatting import GalleryData import AnimatedTextComponent import TelegramUIPreferences +import SegmentControlComponent final class DataUsageScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 3bdffbc9e2..a67ef82cbd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1328,6 +1328,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .paidMessagesPriceEdited: self.interfaceInteraction?.openMonoforum() return true + case .starGiftPurchaseOffer: + let controller = self.context.sharedContext.makeGiftViewScreen(context: self.context, message: EngineMessage(message), shareStory: { [weak self] uniqueGift in + Queue.mainQueue().after(0.15) { + if let self { + let controller = self.context.sharedContext.makeStorySharingScreen(context: self.context, subject: .gift(uniqueGift), parentController: self) + self.push(controller) + } + } + }) + self.push(controller) + return true default: break } @@ -2401,7 +2412,44 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - if let attribute = message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute { + if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction, case let .starGiftPurchaseOffer(gift, amount, _, isAccepted, isDeclined) = action.action, !isAccepted && !isDeclined { + guard let data = data?.makeData(), message.effectivelyIncoming(strongSelf.context.account.peerId) else { + return + } + if data.count != 1 { + return + } + let buttonType = data.withUnsafeBytes { buffer -> UInt8 in + return buffer.baseAddress!.assumingMemoryBound(to: UInt8.self).pointee + } + + switch buttonType { + case 0: + let _ = strongSelf.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: false).startStandalone() + case 1: + let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: message.id.peerId)) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer, case let .unique(gift) = gift else { + return + } + let controller = self.context.sharedContext.makeGiftOfferScreen( + context: self.context, + gift: gift, + peer: peer, + amount: amount, + commit: { [weak self] in + if let self { + let _ = self.context.engine.payments.resolveStarGiftOffer(messageId: message.id, accept: true).startStandalone() + } + } + ) + self.present(controller, in: .window(.root)) + }) + default: + break + } + return + } else if let attribute = message.attributes.first(where: { $0 is SuggestedPostMessageAttribute }) as? SuggestedPostMessageAttribute { guard let data = data?.makeData() else { return } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index b2b08bdccb..83bfb7699f 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -939,13 +939,13 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto context?.account.viewTracker.refreshStoriesForMessageIds(messageIds: Set(messageIds.map(\.messageId))) } self.translationProcessingManager.process = { [weak self, weak context] messageIds in - if let context = context, let translationLang = self?.translationLang { + if let context, let translationLang = self?.translationLang { let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone() } } - self.factCheckProcessingManager.process = { [weak self, weak context] messageIds in - if let context = context, let translationLang = self?.translationLang { - let _ = translateMessageIds(context: context, messageIds: Array(messageIds.map(\.messageId)), fromLang: translationLang.fromLang, toLang: translationLang.toLang).startStandalone() + self.factCheckProcessingManager.process = { [weak context] messageIds in + if let context { + let _ = context.engine.messages.getMessagesFactCheck(messageIds: Array(messageIds.map(\.messageId))).startStandalone() } } @@ -2746,6 +2746,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto messageIdsToTranslate.append(message.id) } else if let _ = message.media.first(where: { $0 is TelegramMediaPoll }) { messageIdsToTranslate.append(message.id) + } else if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending { + messageIdsToTranslate.append(message.id) } case let .MessageGroupEntry(_, messages, _): for (message, _, _, _, _) in messages { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 0a6d10e448..979044b141 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3860,6 +3860,10 @@ public final class SharedAccountContextImpl: SharedAccountContext { return GiftAuctionActiveBidsScreen(context: context) } + public func makeGiftOfferScreen(context: AccountContext, gift: StarGift.UniqueGift, peer: EnginePeer, amount: CurrencyAmount, commit: @escaping () -> Void) -> ViewController { + return giftOfferAlertController(context: context, gift: gift, peer: peer, amount: amount, commit: commit) + } + public func makeStorySharingScreen(context: AccountContext, subject: StorySharingSubject, parentController: ViewController) -> ViewController { let editorSubject: Signal switch subject { diff --git a/submodules/TranslateUI/Sources/ChatTranslation.swift b/submodules/TranslateUI/Sources/ChatTranslation.swift index d799a5a87e..f247b69c61 100644 --- a/submodules/TranslateUI/Sources/ChatTranslation.swift +++ b/submodules/TranslateUI/Sources/ChatTranslation.swift @@ -172,6 +172,11 @@ public func translateMessageIds(context: AccountContext, messageIds: [EngineMess messageIdsToTranslate.append(messageId) messageIdsSet.insert(messageId) } + } else if let audioTranscription = message.attributes.first(where: { $0 is AudioTranscriptionMessageAttribute }) as? AudioTranscriptionMessageAttribute, !audioTranscription.text.isEmpty && !audioTranscription.isPending { + if !messageIdsSet.contains(messageId) { + messageIdsToTranslate.append(messageId) + messageIdsSet.insert(messageId) + } } } else { if !messageIdsSet.contains(messageId) { diff --git a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift index 997a6d3ae0..bde5fba597 100644 --- a/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift +++ b/submodules/WebUI/Sources/WebAppSetEmojiStatusScreen.swift @@ -17,6 +17,7 @@ import AccountContext import PresentationDataUtils import PremiumPeerShortcutComponent import GiftAnimationComponent +import GlassBarButtonComponent private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -58,7 +59,6 @@ private final class SheetContent: CombinedComponent { } final class State: ComponentState { - var cachedCloseImage: (UIImage, PresentationTheme)? } func makeState() -> State { @@ -68,7 +68,7 @@ private final class SheetContent: CombinedComponent { static var body: Body { let background = Child(RoundedRectangle.self) let animation = Child(GiftAnimationComponent.self) - let closeButton = Child(Button.self) + let closeButton = Child(GlassBarButtonComponent.self) let title = Child(Text.self) let text = Child(BalancedTextComponent.self) @@ -78,7 +78,6 @@ private final class SheetContent: CombinedComponent { return { context in let environment = context.environment[EnvironmentType.self] let component = context.component - let state = context.state let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme @@ -108,25 +107,27 @@ private final class SheetContent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: animation.size.height / 2.0 + 12.0)) ) - let closeImage: UIImage - if let (image, cacheTheme) = state.cachedCloseImage, theme === cacheTheme { - closeImage = image - } else { - closeImage = generateCloseButtonImage(backgroundColor: UIColor(rgb: 0x808084, alpha: 0.1), foregroundColor: theme.actionSheet.inputClearButtonColor)! - state.cachedCloseImage = (closeImage, theme) - } let closeButton = closeButton.update( - component: Button( - content: AnyComponent(Image(image: closeImage)), - action: { + component: GlassBarButtonComponent( + size: CGSize(width: 40.0, height: 40.0), + backgroundColor: theme.rootController.navigationBar.glassBarButtonBackgroundColor, + isDark: theme.overallDarkAppearance, + state: .generic, + component: AnyComponentWithIdentity(id: "close", component: AnyComponent( + BundleIconComponent( + name: "Navigation/Close", + tintColor: theme.rootController.navigationBar.glassBarButtonForegroundColor + ) + )), + action: { _ in component.dismiss() } ), - availableSize: CGSize(width: 30.0, height: 30.0), + availableSize: CGSize(width: 40.0, height: 40.0), transition: .immediate ) context.add(closeButton - .position(CGPoint(x: context.availableSize.width - closeButton.size.width, y: 28.0)) + .position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0)) ) let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 @@ -200,10 +201,10 @@ private final class SheetContent: CombinedComponent { let button = button.update( component: ButtonComponent( background: ButtonComponent.Background( + style: .glass, color: theme.list.itemCheckColors.fillColor, foreground: theme.list.itemCheckColors.foregroundColor, pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9), - cornerRadius: 10.0 ), content: AnyComponentWithIdentity( id: AnyHashable(0), @@ -216,7 +217,7 @@ private final class SheetContent: CombinedComponent { controller?.dismissAnimated() } ), - availableSize: CGSize(width: context.availableSize.width - 16.0 * 2.0, height: 50), + availableSize: CGSize(width: context.availableSize.width - 30.0 * 2.0, height: 52.0), transition: .immediate ) context.add(button @@ -298,6 +299,7 @@ private final class WebAppSetEmojiStatusSheetComponent: CombinedComponent { }) } )), + style: .glass, backgroundColor: .color(environment.theme.list.modalBlocksBackgroundColor), followContentSizeChanges: true, clipsContent: true, From 8f1c9019be82d365fa1c412000a636331bcd1300 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 2 Dec 2025 01:43:04 +0800 Subject: [PATCH 05/11] Passkeys --- Telegram/BUILD | 1 + .../AuthorizationSequenceController.swift | 10 +- ...rizationSequencePhoneEntryController.swift | 24 ++- ...tionSequencePhoneEntryControllerNode.swift | 50 ++++- .../PrivacyAndSecurityController.swift | 2 +- .../Sources/Account/Account.swift | 8 +- .../TelegramCore/Sources/Authorization.swift | 182 ++++++++++-------- .../Sources/Network/Network.swift | 16 ++ .../Components/Settings/PasskeysScreen/BUILD | 1 + .../Sources/PasskeysScreen.swift | 4 +- .../PasskeysScreenIntroComponent.swift | 2 +- .../Sources/PasskeysScreenListComponent.swift | 47 ++++- .../Menu/Passkeys.imageset/Contents.json | 12 ++ .../Menu/Passkeys.imageset/key_30.pdf | Bin 0 -> 11847 bytes .../Menu/TwoStepAuth.imageset/Contents.json | 2 +- .../Menu/TwoStepAuth.imageset/Icon-5.pdf | 114 ----------- .../Menu/TwoStepAuth.imageset/lock_30.pdf | Bin 0 -> 11909 bytes .../Resources/Animations/passkey_logo.tgs | Bin 0 -> 44400 bytes 18 files changed, 264 insertions(+), 211 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/key_30.pdf delete mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/Icon-5.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/lock_30.pdf create mode 100644 submodules/TelegramUI/Resources/Animations/passkey_logo.tgs diff --git a/Telegram/BUILD b/Telegram/BUILD index aacae96803..74b3ecae94 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -496,6 +496,7 @@ associated_domains_fragment = "" if telegram_bundle_id not in official_bundle_id applinks:t.me applinks:*.t.me webcredentials:t.me + webcredentials:telegram.org """ diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 9c9c03a5d3..b6d861d6f2 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -324,6 +324,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth accountManager: self.sharedContext.accountManager, account: self.account, passkey: passkey, + foreignDatacenter: nil, forcedPasswordSetupNotice: { value in guard let entry = CodableEntry(ApplicationSpecificCounterNotice(value: value)) else { return nil @@ -332,7 +333,14 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth }, syncContacts: syncContacts ) - |> deliverOnMainQueue).startStrict(next: { _ in + |> deliverOnMainQueue).startStrict(next: { [weak self] result in + guard let self else { + return + } + if result.updatedAccount !== self.account { + self.account = result.updatedAccount + self.inAppPurchaseManager = InAppPurchaseManager(engine: .unauthorized(self.engine)) + } }, error: { [weak self, weak controller] error in Queue.mainQueue().async { if let strongSelf = self, let controller { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index ac22728099..54855476a3 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -157,6 +157,12 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF strongSelf.account = account strongSelf.accountUpdated?(account) } + self.controllerNode.retryPasskey = { [weak self] in + guard let self else { + return + } + self.loadAndPresentPasskey(force: true) + } if let (code, name, number) = self.currentData { self.controllerNode.codeAndNumber = (code, name, number) @@ -194,7 +200,11 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF self.controllerNode.updateCountryCode() } - if #available(iOS 15.0, *) { + self.loadAndPresentPasskey(force: false) + } + + private func loadAndPresentPasskey(force: Bool) { + if #available(iOS 16.0, *) { Task { @MainActor [weak self] in guard let self, let account = self.account else { return @@ -235,7 +245,11 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest]) authController.delegate = self authController.presentationContextProvider = self - authController.performRequests() + if force { + authController.performRequests() + } else { + authController.performRequests(options: [.preferImmediatelyAvailableCredentials]) + } } } } @@ -290,6 +304,12 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF } public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { + if (error as NSError).domain == "com.apple.AuthenticationServices.AuthorizationError" && (error as NSError).code == 1001 { + self.controllerNode.updateDisplayPasskeyLoginOption() + if let validLayout = self.validLayout { + self.containerLayoutUpdated(validLayout, transition: .immediate) + } + } } public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index fafaac93ba..189f8e7c13 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -15,6 +15,7 @@ import TelegramAnimatedStickerNode import SolidRoundedButtonNode import AuthorizationUtils import ManagedAnimationNode +import Markdown private final class PhoneAndCountryNode: ASDisplayNode { let strings: PresentationStrings @@ -312,7 +313,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { private let managedAnimationNode: ManagedPhoneAnimationNode private let titleNode: ASTextNode private let titleActivateAreaNode: AccessibilityAreaNode - private let noticeNode: ASTextNode + private let noticeNode: ImmediateTextNode private let noticeActivateAreaNode: AccessibilityAreaNode private let phoneAndCountryNode: PhoneAndCountryNode private let contactSyncNode: ContactSyncNode @@ -323,6 +324,8 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { private let tokenEventsDisposable = MetaDisposable() var accountUpdated: ((UnauthorizedAccount) -> Void)? + var retryPasskey: (() -> Void)? + private let debugAction: () -> Void var currentNumber: String { @@ -405,7 +408,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.titleActivateAreaNode = AccessibilityAreaNode() self.titleActivateAreaNode.accessibilityTraits = .staticText - self.noticeNode = ASTextNode() + self.noticeNode = ImmediateTextNode() self.noticeNode.maximumNumberOfLines = 0 self.noticeNode.isUserInteractionEnabled = true self.noticeNode.displaysAsynchronously = false @@ -443,6 +446,23 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.addSubnode(self.managedAnimationNode) self.contactSyncNode.isHidden = true + self.noticeNode.highlightAttributeAction = { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { + return NSAttributedString.Key(rawValue: "URL") + } else { + return nil + } + } + self.noticeNode.tapAttributeAction = { [weak self] attributes, _ in + guard let self else { + return + } + if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] as? String { + self.retryPasskey?() + } + } + self.noticeNode.linkHighlightColor = theme.list.itemAccentColor.withAlphaComponent(0.2) + self.phoneAndCountryNode.selectCountryCode = { [weak self] in self?.selectCountryCode?() } @@ -484,7 +504,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { super.didLoad() self.titleNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugTap(_:)))) - #if DEBUG + #if DEBUG && false self.noticeNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.debugQrTap(_:)))) #endif } @@ -555,6 +575,28 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { let _ = self.phoneAndCountryNode.processNumberChange(number: self.phoneAndCountryNode.phoneInputNode.number) } + func updateDisplayPasskeyLoginOption() { + //TODO:localize + if self.account == nil { + return + } + let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Enter your phone number\nor [log in using Passkey >](passkey)", attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemPrimaryTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(17.0), textColor: self.theme.list.itemPrimaryTextColor), + link: MarkdownAttributeSet(font: Font.regular(17.0), textColor: self.theme.list.itemAccentColor), + linkAttribute: { url in + return ("URL", url) + } + ))) + let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: self.theme.list.itemAccentColor) + + if let range = attributedText.string.range(of: ">"), let chevronImage { + attributedText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: attributedText.string)) + } + + self.noticeNode.attributedText = attributedText + } + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var insets = layout.insets(options: []) insets.top = layout.statusBarHeight ?? 20.0 @@ -576,7 +618,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { let noticeInset: CGFloat = self.account == nil ? 32.0 : 0.0 - let noticeSize = self.noticeNode.measure(CGSize(width: min(274.0 + noticeInset, maximumWidth - 28.0), height: CGFloat.greatestFiniteMagnitude)) + let noticeSize = self.noticeNode.updateLayout(CGSize(width: min(274.0 + noticeInset, maximumWidth - 28.0), height: CGFloat.greatestFiniteMagnitude)) let proceedHeight = self.proceedNode.updateLayout(width: maximumWidth - inset * 2.0, transition: transition) let proceedSize = CGSize(width: maximumWidth - inset * 2.0, height: proceedHeight) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index ee04c6c975..c7ec6407bb 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -501,7 +501,7 @@ private enum PrivacyAndSecurityEntry: ItemListNodeEntry { arguments.openTwoStepVerification(data) }) case let .passkeys(_, text, value): - return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/TwoStepAuth")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { + return ItemListDisclosureItem(presentationData: presentationData, systemStyle: .glass, icon: UIImage(bundleImageName: "Settings/Menu/Passkeys")?.precomposed(), title: text, label: value, sectionId: self.section, style: .blocks, action: { arguments.openPasskeys() }) case let .messageAutoremoveTimeout(_, text, value): diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 076f486ed2..262176da07 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -340,7 +340,13 @@ public func accountWithId(accountManager: AccountManager map { network -> AccountResult in return .unauthorized(UnauthorizedAccount(accountManager: accountManager, networkArguments: networkArguments, id: id, rootPath: rootPath, basePath: path, testingEnvironment: beginWithTestingEnvironment, postbox: postbox, network: network, shouldKeepAutoConnection: shouldKeepAutoConnection)) } diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 7d9c14bf2e..db7ce4dd78 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -1183,91 +1183,117 @@ public final class AuthorizationPasskeyData { } } -public func authorizeWithPasskey(accountManager: AccountManager, account: UnauthorizedAccount, passkey: AuthorizationPasskeyData, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?, syncContacts: Bool) -> Signal { - return account.postbox.transaction { transaction -> Signal in - return account.network.request(Api.functions.auth.finishPasskeyLogin(flags: 0, credential: .inputPasskeyCredentialPublicKey(id: passkey.id, rawId: passkey.id, response: .inputPasskeyResponseLogin(clientData: .dataJSON(data: passkey.clientData), authenticatorData: Buffer(data: passkey.authenticatorData), signature: Buffer(data: passkey.signature), userHandle: passkey.userHandle)), fromDcId: nil, fromAuthKeyId: nil), automaticFloodWait: false) - |> map { authorization in - return .authorization(authorization) - } - |> `catch` { error -> Signal in - switch (error.errorCode, error.errorDescription ?? "") { - case (401, "SESSION_PASSWORD_NEEDED"): - return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) - |> mapError { error -> AuthorizationCodeVerificationError in - if error.errorDescription.hasPrefix("FLOOD_WAIT") { - return .limitExceeded - } else { - return .generic - } - } - |> mapToSignal { result -> Signal in - switch result { - case let .password(_, _, _, _, hint, _, _, _, _, _, _): - return .single(.password(hint: hint ?? "")) - } - } - case let (_, errorDescription): - if errorDescription.hasPrefix("FLOOD_WAIT") { - return .fail(.limitExceeded) - } else if errorDescription == "PHONE_CODE_INVALID" || errorDescription == "EMAIL_CODE_INVALID" { - return .fail(.invalidCode) - } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" { - return .fail(.codeExpired) - } else if errorDescription == "PHONE_NUMBER_UNOCCUPIED" { - return .single(.signUp) - } else if errorDescription == "EMAIL_TOKEN_INVALID" { - return .fail(.invalidEmailToken) - } else if errorDescription == "EMAIL_ADDRESS_INVALID" { - return .fail(.invalidEmailAddress) - } else { - return .fail(.generic) - } +public final class AuthorizeWithPasskeyResult { + public let updatedAccount: UnauthorizedAccount + + init(updatedAccount: UnauthorizedAccount) { + self.updatedAccount = updatedAccount + } +} + +public func authorizeWithPasskey(accountManager: AccountManager, account: UnauthorizedAccount, passkey: AuthorizationPasskeyData, foreignDatacenter: (id: Int, authKeyId: Int64)?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?, syncContacts: Bool) -> Signal { + let userHandle = passkey.userHandle.components(separatedBy: ":") + var targetDatacenterId: Int? + if foreignDatacenter == nil && userHandle.count >= 2 { + targetDatacenterId = Int(userHandle[0]) + } + + if let targetDatacenterId, account.masterDatacenterId != Int32(targetDatacenterId) { + let initialDatacenterId = account.masterDatacenterId + return account.network.getAuthKeyId() + |> castError(AuthorizationCodeVerificationError.self) + |> mapToSignal { sourceAuthKeyId -> Signal in + let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: Int32(targetDatacenterId)) + return updatedAccount + |> mapToSignalPromotingError { updatedAccount -> Signal in + return authorizeWithPasskey(accountManager: accountManager, account: updatedAccount, passkey: passkey, foreignDatacenter: (Int(initialDatacenterId), sourceAuthKeyId), forcedPasswordSetupNotice: forcedPasswordSetupNotice, syncContacts: syncContacts) } } - |> mapToSignal { result -> Signal in - return account.postbox.transaction { transaction -> Signal in - switch result { - case .signUp: - return .fail(.generic) - case let .password(hint): - transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: nil, code: nil, suggestReset: false, syncContacts: syncContacts))) - return .single(.loggedIn) - case let .authorization(authorization): - switch authorization { - case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user): - if let futureAuthToken = futureAuthToken { - storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) - } - - let user = TelegramUser(user: user) - var isSupportUser = false - if let phone = user.phone, phone.hasPrefix("42") { - isSupportUser = true - } - let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) - initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) - transaction.setState(state) - if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) { - transaction.setNoticeEntry(key: value.0, value: value.1) - } - return accountManager.transaction { transaction -> AuthorizeWithCodeResult in - switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser) - return .loggedIn - } - |> castError(AuthorizationCodeVerificationError.self) - case .authorizationSignUpRequired: - return .fail(.generic) + } + + var flags: Int32 = 0 + if foreignDatacenter != nil { + flags |= 1 << 0 + } + return account.network.request(Api.functions.auth.finishPasskeyLogin(flags: flags, credential: .inputPasskeyCredentialPublicKey(id: passkey.id, rawId: passkey.id, response: .inputPasskeyResponseLogin(clientData: .dataJSON(data: passkey.clientData), authenticatorData: Buffer(data: passkey.authenticatorData), signature: Buffer(data: passkey.signature), userHandle: passkey.userHandle)), fromDcId: (foreignDatacenter?.id).flatMap(Int32.init), fromAuthKeyId: foreignDatacenter?.authKeyId), automaticFloodWait: false) + |> map { authorization in + return .authorization(authorization) + } + |> `catch` { error -> Signal in + switch (error.errorCode, error.errorDescription ?? "") { + case (401, "SESSION_PASSWORD_NEEDED"): + return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) + |> mapError { error -> AuthorizationCodeVerificationError in + if error.errorDescription.hasPrefix("FLOOD_WAIT") { + return .limitExceeded + } else { + return .generic } } - } - |> mapError { _ -> AuthorizationCodeVerificationError in - } - |> switchToLatest + |> mapToSignal { result -> Signal in + switch result { + case let .password(_, _, _, _, hint, _, _, _, _, _, _): + return .single(.password(hint: hint ?? "")) + } + } + case let (_, errorDescription): + if errorDescription.hasPrefix("FLOOD_WAIT") { + return .fail(.limitExceeded) + } else if errorDescription == "PHONE_CODE_INVALID" || errorDescription == "EMAIL_CODE_INVALID" { + return .fail(.invalidCode) + } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" { + return .fail(.codeExpired) + } else if errorDescription == "PHONE_NUMBER_UNOCCUPIED" { + return .single(.signUp) + } else if errorDescription == "EMAIL_TOKEN_INVALID" { + return .fail(.invalidEmailToken) + } else if errorDescription == "EMAIL_ADDRESS_INVALID" { + return .fail(.invalidEmailAddress) + } else { + return .fail(.generic) + } } } - |> mapError { _ -> AuthorizationCodeVerificationError in + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction -> Signal in + switch result { + case .signUp: + return .fail(.generic) + case let .password(hint): + transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: nil, code: nil, suggestReset: false, syncContacts: syncContacts))) + return .single(AuthorizeWithPasskeyResult(updatedAccount: account)) + case let .authorization(authorization): + switch authorization { + case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user): + if let futureAuthToken = futureAuthToken { + storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) + } + + let user = TelegramUser(user: user) + var isSupportUser = false + if let phone = user.phone, phone.hasPrefix("42") { + isSupportUser = true + } + let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) + initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) + transaction.setState(state) + if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) { + transaction.setNoticeEntry(key: value.0, value: value.1) + } + return accountManager.transaction { transaction -> AuthorizeWithPasskeyResult in + switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser) + return AuthorizeWithPasskeyResult(updatedAccount: account) + } + |> castError(AuthorizationCodeVerificationError.self) + case .authorizationSignUpRequired: + return .fail(.generic) + } + } + } + |> mapError { _ -> AuthorizationCodeVerificationError in + } + |> switchToLatest } - |> switchToLatest } public enum PasswordRecoveryRequestError { diff --git a/submodules/TelegramCore/Sources/Network/Network.swift b/submodules/TelegramCore/Sources/Network/Network.swift index f9dd1d6c1e..75e53aa7b6 100644 --- a/submodules/TelegramCore/Sources/Network/Network.swift +++ b/submodules/TelegramCore/Sources/Network/Network.swift @@ -1065,6 +1065,22 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate { } } + public func getAuthKeyId() -> Signal { + let mtContext = self.mtProto.context + let datacenterId = self.datacenterId + return Signal { subscriber in + MTContext.contextQueue().dispatch(onQueue: { + var result: Int64 = 0 + if let authInfo = mtContext?.authInfoForDatacenter(withId: datacenterId, selector: .persistent) { + result = authInfo.authKeyId + } + subscriber.putNext(result) + }) + + return EmptyDisposable + } + } + public func requestWithAdditionalInfo(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse), info: NetworkRequestAdditionalInfo, tag: NetworkRequestDependencyTag? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal, MTRpcError> { let requestService = self.requestService return Signal { subscriber in diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD index 0c4a7d0c38..38a7d38057 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/TelegramUI/Components/LottieComponent", "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", + "//submodules/TelegramUI/Components/EmojiStatusComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift index c5fd773915..329071ad5d 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift @@ -169,12 +169,12 @@ final class PasskeysScreenComponent: Component { } let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) //TODO:localize - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Passkey?", text: "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too.", actions: [TextAlertAction(type: .destructiveAction, title: "Delete", action: { [weak self] in + controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: "Delete Passkey?", text: "Once deleted, this passkey can't be used to log in.\n\nDon't forget to remove it from your password manager too.", actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .destructiveAction, title: "Delete", action: { [weak self] in guard let self else { return } self.deletePasskey(id: id) - }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { })]), in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift index e87589c9fc..bb49ae5cd1 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift @@ -114,7 +114,7 @@ final class PasskeysScreenIntroComponent: Component { let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: "TwoFactorSetupIntro"), + content: LottieComponent.AppBundleContent(name: "passkey_logo"), loop: false )), environment: {}, diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift index 4cd18eedd6..dfafe38578 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift @@ -12,6 +12,7 @@ import BundleIconComponent import ListSectionComponent import ListActionItemComponent import TelegramCore +import EmojiStatusComponent final class PasskeysScreenListComponent: Component { let context: AccountContext @@ -112,7 +113,7 @@ final class PasskeysScreenListComponent: Component { let iconSize = self.icon.update( transition: .immediate, component: AnyComponent(LottieComponent( - content: LottieComponent.AppBundleContent(name: "TwoFactorSetupIntro"), + content: LottieComponent.AppBundleContent(name: "passkey_logo"), loop: false )), environment: {}, @@ -184,6 +185,43 @@ final class PasskeysScreenListComponent: Component { dateFormatter.dateStyle = .medium let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(passkey.date))) + let iconComponent: AnyComponentWithIdentity + if let emojiId = passkey.emojiId { + iconComponent = AnyComponentWithIdentity( + id: "lottie", + component: AnyComponent(TransformContents( + content: AnyComponent(EmojiStatusComponent( + context: component.context, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer, + content: .animation( + content: .customEmoji(fileId: emojiId), + size: CGSize(width: 40.0, height: 40.0), + placeholderColor: component.theme.list.mediaPlaceholderColor, + themeColor: nil, + loopMode: .count(1) + ), + size: CGSize(width: 40.0, height: 40.0), + isVisibleForAnimations: true, + action: nil + )), + translation: CGPoint(x: 0.0, y: 1.0) + )) + ) + } else { + iconComponent = AnyComponentWithIdentity( + id: "icon", + component: AnyComponent(BundleIconComponent(name: "Settings/Menu/Passkeys", tintColor: nil)) + ) + } + + //TODO:localize + var subtitleString = "created \(dateString)" + if let lastUsageDate = passkey.lastUsageDate { + let lastUsedDateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(lastUsageDate))) + subtitleString.append(" • used \(lastUsedDateString)") + } + listSectionItems.append(AnyComponentWithIdentity(id: passkey.id, component: AnyComponent(ListActionItemComponent( theme: component.theme, title: AnyComponent(VStack([ @@ -197,7 +235,7 @@ final class PasskeysScreenListComponent: Component { ))), AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "created \(dateString)", + string: subtitleString, font: Font.regular(14.0), textColor: component.theme.list.itemSecondaryTextColor )), @@ -205,10 +243,7 @@ final class PasskeysScreenListComponent: Component { ))) ], alignment: .left, spacing: 2.0)), leftIcon: .custom( - AnyComponentWithIdentity( - id: "icon", - component: AnyComponent(BundleIconComponent(name: "Settings/Menu/TwoStepAuth", tintColor: nil)) - ), + iconComponent, false ), accessory: nil, diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/Contents.json new file mode 100644 index 0000000000..be5de9e9a0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "key_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/key_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/Passkeys.imageset/key_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..301727da150c80ea8c59baa02bff657057ad7797 GIT binary patch literal 11847 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 AeE> -endobj - -2 0 obj - << /Length 3 0 R >> -stream -/DeviceRGB CS -/DeviceRGB cs -q -1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm -1.000000 0.584314 0.000000 scn -0.000000 18.799999 m -0.000000 22.720367 0.000000 24.680552 0.762954 26.177933 c -1.434068 27.495068 2.504932 28.565931 3.822066 29.237045 c -5.319448 30.000000 7.279633 30.000000 11.200000 30.000000 c -18.799999 30.000000 l -22.720367 30.000000 24.680552 30.000000 26.177933 29.237045 c -27.495068 28.565931 28.565931 27.495068 29.237045 26.177933 c -30.000000 24.680552 30.000000 22.720367 30.000000 18.799999 c -30.000000 11.200001 l -30.000000 7.279633 30.000000 5.319448 29.237045 3.822067 c -28.565931 2.504932 27.495068 1.434069 26.177933 0.762955 c -24.680552 0.000000 22.720367 0.000000 18.799999 0.000000 c -11.200000 0.000000 l -7.279633 0.000000 5.319448 0.000000 3.822066 0.762955 c -2.504932 1.434069 1.434068 2.504932 0.762954 3.822067 c -0.000000 5.319448 0.000000 7.279633 0.000000 11.200001 c -0.000000 18.799999 l -h -f -n -Q -q --1.000000 -0.000000 -0.000000 1.000000 25.000000 6.000000 cm -1.000000 1.000000 1.000000 scn -6.650000 19.000000 m -2.945000 19.000000 0.000000 16.055000 0.000000 12.350000 c -0.000000 8.645000 2.945000 5.700001 6.650000 5.700001 c -7.500165 5.700001 8.310955 5.861581 9.054688 6.145313 c -10.450001 4.750000 l -12.350000 4.750000 l -12.350000 2.850000 l -14.250000 2.850000 l -14.250000 0.950001 l -14.903126 0.296875 l -15.093125 0.106874 15.300938 0.000000 15.585938 0.000000 c -18.049999 0.000000 l -18.619999 0.000000 19.000000 0.380001 19.000000 0.950001 c -19.000000 3.414062 l -19.000000 3.699062 18.893126 3.906875 18.703125 4.096874 c -12.854687 9.945312 l -13.138419 10.689045 13.299999 11.499835 13.299999 12.350000 c -13.299999 16.055000 10.355000 19.000000 6.650000 19.000000 c -h -5.225000 16.150000 m -6.555000 16.150000 7.600000 15.105000 7.600000 13.775000 c -7.600000 12.445000 6.555000 11.400000 5.225000 11.400000 c -3.895000 11.400000 2.850000 12.445000 2.850000 13.775000 c -2.850000 15.105000 3.895000 16.150000 5.225000 16.150000 c -h -f* -n -Q - -endstream -endobj - -3 0 obj - 1987 -endobj - -4 0 obj - << /Annots [] - /Type /Page - /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] - /Resources 1 0 R - /Contents 2 0 R - /Parent 5 0 R - >> -endobj - -5 0 obj - << /Kids [ 4 0 R ] - /Count 1 - /Type /Pages - >> -endobj - -6 0 obj - << /Type /Catalog - /Pages 5 0 R - >> -endobj - -xref -0 7 -0000000000 65535 f -0000000010 00000 n -0000000034 00000 n -0000002077 00000 n -0000002100 00000 n -0000002273 00000 n -0000002347 00000 n -trailer -<< /ID [ (some) (id) ] - /Root 6 0 R - /Size 7 ->> -startxref -2406 -%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/lock_30.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/TwoStepAuth.imageset/lock_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b1f6efd7ab03b2d4fe446927b793d10b37ba808a GIT binary patch literal 11909 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 z%0A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 M0ebyUnEB5G0E!1?{Qv*} literal 0 HcmV?d00001 From 2967648b2b0957e2a780f2a6191dd3083ca4dd84 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 2 Dec 2025 01:51:40 +0800 Subject: [PATCH 06/11] Update icons --- .../PasskeysScreen/Sources/PasskeysScreen.swift | 2 +- .../Sources/PasskeysScreenIntroComponent.swift | 6 +++--- .../Settings/Passkeys/Contents.json | 9 +++++++++ .../Passkeys/Intro1.imageset/Contents.json | 12 ++++++++++++ .../Settings/Passkeys/Intro1.imageset/key.pdf | Bin 0 -> 5170 bytes .../Passkeys/Intro2.imageset/Contents.json | 12 ++++++++++++ .../Settings/Passkeys/Intro2.imageset/faceid.pdf | Bin 0 -> 5921 bytes .../Passkeys/Intro3.imageset/Contents.json | 12 ++++++++++++ .../Settings/Passkeys/Intro3.imageset/lock.pdf | Bin 0 -> 5021 bytes 9 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/key.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro2.imageset/faceid.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro3.imageset/lock.pdf diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift index 329071ad5d..bedfb253ea 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift @@ -204,7 +204,7 @@ final class PasskeysScreenComponent: Component { } if let credentialId = decodeBase64(passkey.id) { do { - try await updater.reportUnknownPublicKeyCredential(relyingPartyIdentifier: "t.me", credentialID: credentialId) + try await updater.reportUnknownPublicKeyCredential(relyingPartyIdentifier: "telegram.org", credentialID: credentialId) } catch let e { Logger.shared.log("Passkeys", "reportUnknownPublicKeyCredential error: \(e)") } diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift index bb49ae5cd1..28df5748a5 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenIntroComponent.swift @@ -182,17 +182,17 @@ final class PasskeysScreenIntroComponent: Component { } let itemDescs: [ItemDesc] = [ ItemDesc( - icon: "Chat List/Archive/IconArchived", + icon: "Settings/Passkeys/Intro1", title: "Create a Passkey", text: "Make a passkey to sign in easily and safely." ), ItemDesc( - icon: "Chat List/Archive/IconHide", + icon: "Settings/Passkeys/Intro2", title: "Log in with Face ID", text: "Use Face ID, Touch ID, or your passcode to sign in." ), ItemDesc( - icon: "Chat List/Archive/IconStories", + icon: "Settings/Passkeys/Intro3", title: "Store Passkey Securely", text: "Your passkey is safely kept in your iCloud Keychain." ) diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/Contents.json new file mode 100644 index 0000000000..7ee24619ad --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "key.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/key.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Passkeys/Intro1.imageset/key.pdf new file mode 100644 index 0000000000000000000000000000000000000000..29928ca1db021dc0ac96449916dfaa3cc92f376f GIT binary patch literal 5170 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 HFx31%;?$b( literal 0 HcmV?d00001 From 5046aab8ff38f637d2fb74ced7420569d9a66daf Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 2 Dec 2025 12:36:05 +0400 Subject: [PATCH 07/11] Update API --- .../Telegram-iOS/en.lproj/Localizable.strings | 34 ++ .../Sources/AccountContext.swift | 3 +- .../Sources/ChannelAdminController.swift | 29 +- submodules/TelegramApi/Sources/Api0.swift | 23 +- submodules/TelegramApi/Sources/Api15.swift | 18 +- submodules/TelegramApi/Sources/Api23.swift | 18 +- submodules/TelegramApi/Sources/Api25.swift | 314 ++++++------- submodules/TelegramApi/Sources/Api26.swift | 356 ++++++--------- submodules/TelegramApi/Sources/Api27.swift | 224 +++++++++ submodules/TelegramApi/Sources/Api36.swift | 138 +++--- submodules/TelegramApi/Sources/Api37.swift | 122 ++--- submodules/TelegramApi/Sources/Api38.swift | 74 +++ submodules/TelegramApi/Sources/Api39.swift | 15 + .../Sources/GiftAuctionAccessoryPanel.swift | 20 +- .../ApiUtils/TelegramMediaAction.swift | 4 +- .../Sources/State/ManagedRecentStickers.swift | 1 + .../SyncCore/SyncCore_Namespaces.swift | 1 + .../SyncCore_TelegramMediaAction.swift | 13 +- .../TelegramEngine/Payments/StarGifts.swift | 179 +++++++- .../Payments/StarGiftsAuctions.swift | 75 ++- .../TelegramEngine/Payments/Stars.swift | 3 +- .../Payments/TelegramEnginePayments.swift | 4 + .../Sources/ServiceMessageStrings.swift | 2 +- .../ChatMessageGiftBubbleContentNode.swift | 4 +- .../ChatMessageWebpageBubbleContentNode.swift | 18 +- .../EmojiStatusSelectionComponent.swift | 2 +- .../Sources/GiftItemComponent.swift | 10 +- .../Sources/GiftOptionsScreen.swift | 17 +- .../Components/Gifts/GiftSetupScreen/BUILD | 2 +- .../Sources/ChatGiftPreviewItem.swift | 2 +- .../Sources/GiftSetupScreen.swift | 10 +- .../Sources/GiftAuctionAcquiredScreen.swift | 12 +- .../Sources/GiftAuctionActiveBidsScreen.swift | 2 +- .../Sources/GiftAuctionBidScreen.swift | 310 +++++++++---- .../Sources/GiftAuctionInfoScreen.swift | 2 +- .../GiftAuctionUpgradePreviewScreen.swift | 427 ++++++++++++++++++ .../Sources/GiftAuctionViewScreen.swift | 113 +++-- .../Sources/GiftViewScreen.swift | 87 ++-- .../Sources/UserApperanceScreen.swift | 1 + .../Sources/SharedAccountContext.swift | 9 +- 40 files changed, 1885 insertions(+), 813 deletions(-) create mode 100644 submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7ce787238b..0531fec09e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15466,3 +15466,37 @@ Error: %8$@"; "CreateExternalStream.RevokeStreamKey" = "Revoke Stream Key"; "CreateExternalStream.Revoke.Text" = "Do you want to revoke the stream key?"; "CreateExternalStream.Revoke.Revoke" = "Revoke"; + +"Gift.AuctionBid.UntilEnd" = "until the end"; +"Gift.AuctionBid.BeforeStart" = "before start"; +"Gift.AuctionBid.RoundSubtitle" = "Round %@ of %@"; +"Gift.AuctionBid.TopWinnersTotal" = "Top 3 of %@ Winners"; + +"Gift.Acquired.GiftRound" = "%1$@ in round %2$@"; + +"Gift.Options.Gift.Soon" = "soon"; + +"Chat.Auction.StartsIn" = "starts in "; +"Chat.Auction.View" = "VIEW"; +"Chat.Auction.Upcoming" = "Upcoming Auction"; + +"Gift.Auction.Start" = "Start"; +"Gift.Auction.End" = "End"; +"Gift.Auction.Quantity" = "Quantity"; +"Gift.Auction.TotalRounds" = "Rounds"; +"Gift.Auction.TimeRound" = "Round %@"; +"Gift.Auction.TimeRounds" = "Rounds %@"; +"Gift.Auction.StartsInHours" = "auction starts in {h}:{m}:{s}"; +"Gift.Auction.StartsInMinutes" = "auction starts in {m}:{s}"; +"Gift.Auction.EarlyBid" = "Place an Early Bid"; + +"Gift.Auction.Hours_1" = "%@ hour"; +"Gift.Auction.Hours_any" = "%@ hours"; +"Gift.Auction.Minutes_1" = "%@ minute"; +"Gift.Auction.Minutes_any" = "%@ minutes"; +"Gift.Auction.HoursEach_1" = "%@ hour each"; +"Gift.Auction.HoursEach_any" = "%@ hours each"; +"Gift.Auction.MinutesEach_1" = "%@ minute each"; +"Gift.Auction.MinutesEach_any" = "%@ minutes each"; + +"ChatList.Auctions.UpcomingAuction" = "Upcoming Auction"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 1a1aa391b2..e370b49404 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1422,7 +1422,8 @@ public protocol SharedAccountContext: AnyObject { func makeStarsIntroScreen(context: AccountContext) -> ViewController func makeGiftViewScreen(context: AccountContext, message: EngineMessage, shareStory: ((StarGift.UniqueGift) -> Void)?) -> ViewController func makeGiftViewScreen(context: AccountContext, gift: StarGift.UniqueGift, shareStory: ((StarGift.UniqueGift) -> Void)?, openChatTheme: (() -> Void)?, dismissed: (() -> Void)?) -> ViewController - func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController + func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]?) -> ViewController + func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController func makeGiftAuctionBidScreen(context: AccountContext, toPeerId: EnginePeer.Id, text: String?, entities: [MessageTextEntity]?, hideName: Bool, auctionContext: GiftAuctionContext, acquiredGifts: Signal<[GiftAuctionAcquiredGift], NoError>?) -> ViewController func makeGiftAuctionViewScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: @escaping (Signal<[GiftAuctionAcquiredGift], NoError>) -> Void) -> ViewController diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index d4f2c23ef0..deb1d436b9 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -548,34 +548,6 @@ private func stringForRight(strings: PresentationStrings, right: TelegramChatAdm } } -private func rightDependencies(_ right: TelegramChatAdminRightsFlags) -> [TelegramChatAdminRightsFlags] { - if right.contains(.canChangeInfo) { - return [] - } else if right.contains(.canPostMessages) { - return [] - } else if right.contains(.canEditMessages) { - return [] - } else if right.contains(.canDeleteMessages) { - return [] - } else if right.contains(.canBanUsers) { - return [] - } else if right.contains(.canInviteUsers) { - return [] - } else if right.contains(.canPinMessages) { - return [] - } else if right.contains(.canAddAdmins) { - return [] - } else if right.contains(.canManageDirect) { - return [] - } else if right.contains(.canManageCalls) { - return [] - } else if right.contains(.canBeAnonymous) { - return [] - } else { - return [] - } -} - private func canEditAdminRights(accountPeerId: EnginePeer.Id, channelPeer: EnginePeer, initialParticipant: ChannelParticipant?) -> Bool { if case let .channel(channel) = channelPeer { if channel.flags.contains(.isCreator) { @@ -658,6 +630,7 @@ private func channelAdminControllerEntries(presentationData: PresentationData, s .direct(.canChangeInfo), .sub(.messages, messageRelatedFlags), .sub(.stories, storiesRelatedFlags), + .direct(.canBanUsers), .direct(.canInviteUsers), .direct(.canManageDirect), .direct(.canManageCalls), diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index fc4b470207..8c9f67a14d 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -620,7 +620,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1189364422] = { return Api.MessageAction.parse_messageActionSetChatTheme($0) } dict[1348510708] = { return Api.MessageAction.parse_messageActionSetChatWallPaper($0) } dict[1007897979] = { return Api.MessageAction.parse_messageActionSetMessagesTTL($0) } - dict[-614898352] = { return Api.MessageAction.parse_messageActionStarGift($0) } + dict[-366202413] = { return Api.MessageAction.parse_messageActionStarGift($0) } dict[2000845012] = { return Api.MessageAction.parse_messageActionStarGiftPurchaseOffer($0) } dict[1940760427] = { return Api.MessageAction.parse_messageActionStarGiftPurchaseOfferDeclined($0) } dict[-1787656893] = { return Api.MessageAction.parse_messageActionStarGiftUnique($0) } @@ -915,7 +915,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1681948327] = { return Api.SavedDialog.parse_monoForumDialog($0) } dict[-1115174036] = { return Api.SavedDialog.parse_savedDialog($0) } dict[-881854424] = { return Api.SavedReactionTag.parse_savedReactionTag($0) } - dict[-1987861422] = { return Api.SavedStarGift.parse_savedStarGift($0) } + dict[-355041186] = { return Api.SavedStarGift.parse_savedStarGift($0) } dict[1040931690] = { return Api.SearchPostsFlood.parse_searchPostsFlood($0) } dict[-911191137] = { return Api.SearchResultsCalendarPeriod.parse_searchResultsCalendarPeriod($0) } dict[2137295719] = { return Api.SearchResultsPosition.parse_searchResultPosition($0) } @@ -980,8 +980,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[2109703795] = { return Api.SponsoredMessage.parse_sponsoredMessage($0) } dict[1124938064] = { return Api.SponsoredMessageReportOption.parse_sponsoredMessageReportOption($0) } dict[-963180333] = { return Api.SponsoredPeer.parse_sponsoredPeer($0) } - dict[463097215] = { return Api.StarGift.parse_starGift($0) } - dict[-1624264554] = { return Api.StarGift.parse_starGiftUnique($0) } + dict[1453798502] = { return Api.StarGift.parse_starGift($0) } + dict[1453155529] = { return Api.StarGift.parse_starGiftUnique($0) } dict[-753154979] = { return Api.StarGiftActiveAuctionState.parse_starGiftActiveAuctionState($0) } dict[-650279524] = { return Api.StarGiftAttribute.parse_starGiftAttributeBackdrop($0) } dict[970559507] = { return Api.StarGiftAttribute.parse_starGiftAttributeModel($0) } @@ -991,8 +991,10 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[520210263] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdBackdrop($0) } dict[1219145276] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdModel($0) } dict[1242965043] = { return Api.StarGiftAttributeId.parse_starGiftAttributeIdPattern($0) } - dict[-1419714037] = { return Api.StarGiftAuctionAcquiredGift.parse_starGiftAuctionAcquiredGift($0) } - dict[1571835723] = { return Api.StarGiftAuctionState.parse_starGiftAuctionState($0) } + dict[1118831432] = { return Api.StarGiftAuctionAcquiredGift.parse_starGiftAuctionAcquiredGift($0) } + dict[984483112] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRound($0) } + dict[178266597] = { return Api.StarGiftAuctionRound.parse_starGiftAuctionRoundExtendable($0) } + dict[1998212710] = { return Api.StarGiftAuctionState.parse_starGiftAuctionState($0) } dict[2107014202] = { return Api.StarGiftAuctionState.parse_starGiftAuctionStateFinished($0) } dict[-30197422] = { return Api.StarGiftAuctionState.parse_starGiftAuctionStateNotModified($0) } dict[787403204] = { return Api.StarGiftAuctionUserState.parse_starGiftAuctionUserState($0) } @@ -1493,12 +1495,13 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1803939105] = { return Api.payments.ResaleStarGifts.parse_resaleStarGifts($0) } dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) } dict[-1779201615] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) } - dict[-1745778728] = { return Api.payments.StarGiftActiveAuctions.parse_starGiftActiveAuctions($0) } + dict[-1359565892] = { return Api.payments.StarGiftActiveAuctions.parse_starGiftActiveAuctions($0) } dict[-617358640] = { return Api.payments.StarGiftActiveAuctions.parse_starGiftActiveAuctionsNotModified($0) } dict[2103169520] = { return Api.payments.StarGiftAuctionAcquiredGifts.parse_starGiftAuctionAcquiredGifts($0) } - dict[244900980] = { return Api.payments.StarGiftAuctionState.parse_starGiftAuctionState($0) } + dict[1798960364] = { return Api.payments.StarGiftAuctionState.parse_starGiftAuctionState($0) } dict[-1977011469] = { return Api.payments.StarGiftCollections.parse_starGiftCollections($0) } dict[-1598402793] = { return Api.payments.StarGiftCollections.parse_starGiftCollectionsNotModified($0) } + dict[1187439471] = { return Api.payments.StarGiftUpgradeAttributes.parse_starGiftUpgradeAttributes($0) } dict[1038213101] = { return Api.payments.StarGiftUpgradePreview.parse_starGiftUpgradePreview($0) } dict[-2069218660] = { return Api.payments.StarGiftWithdrawalUrl.parse_starGiftWithdrawalUrl($0) } dict[785918357] = { return Api.payments.StarGifts.parse_starGifts($0) } @@ -2243,6 +2246,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.StarGiftAuctionAcquiredGift: _1.serialize(buffer, boxed) + case let _1 as Api.StarGiftAuctionRound: + _1.serialize(buffer, boxed) case let _1 as Api.StarGiftAuctionState: _1.serialize(buffer, boxed) case let _1 as Api.StarGiftAuctionUserState: @@ -2677,6 +2682,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.payments.StarGiftCollections: _1.serialize(buffer, boxed) + case let _1 as Api.payments.StarGiftUpgradeAttributes: + _1.serialize(buffer, boxed) case let _1 as Api.payments.StarGiftUpgradePreview: _1.serialize(buffer, boxed) case let _1 as Api.payments.StarGiftWithdrawalUrl: diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index a526ce3c8c..4bb0329f83 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -1069,7 +1069,7 @@ public extension Api { case messageActionSetChatTheme(theme: Api.ChatTheme) case messageActionSetChatWallPaper(flags: Int32, wallpaper: Api.WallPaper) case messageActionSetMessagesTTL(flags: Int32, period: Int32, autoSettingFrom: Int64?) - case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, prepaidUpgradeHash: String?, giftMsgId: Int32?, toId: Api.Peer?) + case messageActionStarGift(flags: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, convertStars: Int64?, upgradeMsgId: Int32?, upgradeStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, prepaidUpgradeHash: String?, giftMsgId: Int32?, toId: Api.Peer?, giftNum: Int32?) case messageActionStarGiftPurchaseOffer(flags: Int32, gift: Api.StarGift, price: Api.StarsAmount, expiresAt: Int32) case messageActionStarGiftPurchaseOfferDeclined(flags: Int32, gift: Api.StarGift, price: Api.StarsAmount) case messageActionStarGiftUnique(flags: Int32, gift: Api.StarGift, canExportAt: Int32?, transferStars: Int64?, fromId: Api.Peer?, peer: Api.Peer?, savedId: Int64?, resaleAmount: Api.StarsAmount?, canTransferAt: Int32?, canResellAt: Int32?, dropOriginalDetailsStars: Int64?) @@ -1462,9 +1462,9 @@ public extension Api { serializeInt32(period, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 0) != 0 {serializeInt64(autoSettingFrom!, buffer: buffer, boxed: false)} break - case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId): + case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId, let giftNum): if boxed { - buffer.appendInt32(-614898352) + buffer.appendInt32(-366202413) } serializeInt32(flags, buffer: buffer, boxed: false) gift.serialize(buffer, true) @@ -1478,6 +1478,7 @@ public extension Api { if Int(flags) & Int(1 << 14) != 0 {serializeString(prepaidUpgradeHash!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 15) != 0 {serializeInt32(giftMsgId!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 18) != 0 {toId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 19) != 0 {serializeInt32(giftNum!, buffer: buffer, boxed: false)} break case .messageActionStarGiftPurchaseOffer(let flags, let gift, let price, let expiresAt): if boxed { @@ -1697,8 +1698,8 @@ public extension Api { return ("messageActionSetChatWallPaper", [("flags", flags as Any), ("wallpaper", wallpaper as Any)]) case .messageActionSetMessagesTTL(let flags, let period, let autoSettingFrom): return ("messageActionSetMessagesTTL", [("flags", flags as Any), ("period", period as Any), ("autoSettingFrom", autoSettingFrom as Any)]) - case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId): - return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("giftMsgId", giftMsgId as Any), ("toId", toId as Any)]) + case .messageActionStarGift(let flags, let gift, let message, let convertStars, let upgradeMsgId, let upgradeStars, let fromId, let peer, let savedId, let prepaidUpgradeHash, let giftMsgId, let toId, let giftNum): + return ("messageActionStarGift", [("flags", flags as Any), ("gift", gift as Any), ("message", message as Any), ("convertStars", convertStars as Any), ("upgradeMsgId", upgradeMsgId as Any), ("upgradeStars", upgradeStars as Any), ("fromId", fromId as Any), ("peer", peer as Any), ("savedId", savedId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("giftMsgId", giftMsgId as Any), ("toId", toId as Any), ("giftNum", giftNum as Any)]) case .messageActionStarGiftPurchaseOffer(let flags, let gift, let price, let expiresAt): return ("messageActionStarGiftPurchaseOffer", [("flags", flags as Any), ("gift", gift as Any), ("price", price as Any), ("expiresAt", expiresAt as Any)]) case .messageActionStarGiftPurchaseOfferDeclined(let flags, let gift, let price): @@ -2477,6 +2478,8 @@ public extension Api { if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() { _12 = Api.parse(reader, signature: signature) as? Api.Peer } } + var _13: Int32? + if Int(_1!) & Int(1 << 19) != 0 {_13 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil @@ -2489,8 +2492,9 @@ public extension Api { let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil let _c11 = (Int(_1!) & Int(1 << 15) == 0) || _11 != nil let _c12 = (Int(_1!) & Int(1 << 18) == 0) || _12 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { - return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, upgradeMsgId: _5, upgradeStars: _6, fromId: _7, peer: _8, savedId: _9, prepaidUpgradeHash: _10, giftMsgId: _11, toId: _12) + let _c13 = (Int(_1!) & Int(1 << 19) == 0) || _13 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 { + return Api.MessageAction.messageActionStarGift(flags: _1!, gift: _2!, message: _3, convertStars: _4, upgradeMsgId: _5, upgradeStars: _6, fromId: _7, peer: _8, savedId: _9, prepaidUpgradeHash: _10, giftMsgId: _11, toId: _12, giftNum: _13) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 1c602ab476..de67bb1fd4 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -626,13 +626,13 @@ public extension Api { } public extension Api { enum SavedStarGift: TypeConstructorDescription { - case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?, collectionId: [Int32]?, prepaidUpgradeHash: String?, dropOriginalDetailsStars: Int64?) + case savedStarGift(flags: Int32, fromId: Api.Peer?, date: Int32, gift: Api.StarGift, message: Api.TextWithEntities?, msgId: Int32?, savedId: Int64?, convertStars: Int64?, upgradeStars: Int64?, canExportAt: Int32?, transferStars: Int64?, canTransferAt: Int32?, canResellAt: Int32?, collectionId: [Int32]?, prepaidUpgradeHash: String?, dropOriginalDetailsStars: Int64?, giftNum: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars): + case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars, let giftNum): if boxed { - buffer.appendInt32(-1987861422) + buffer.appendInt32(-355041186) } serializeInt32(flags, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {fromId!.serialize(buffer, true)} @@ -654,14 +654,15 @@ public extension Api { }} if Int(flags) & Int(1 << 16) != 0 {serializeString(prepaidUpgradeHash!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 18) != 0 {serializeInt64(dropOriginalDetailsStars!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 19) != 0 {serializeInt32(giftNum!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars): - return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any), ("collectionId", collectionId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("dropOriginalDetailsStars", dropOriginalDetailsStars as Any)]) + case .savedStarGift(let flags, let fromId, let date, let gift, let message, let msgId, let savedId, let convertStars, let upgradeStars, let canExportAt, let transferStars, let canTransferAt, let canResellAt, let collectionId, let prepaidUpgradeHash, let dropOriginalDetailsStars, let giftNum): + return ("savedStarGift", [("flags", flags as Any), ("fromId", fromId as Any), ("date", date as Any), ("gift", gift as Any), ("message", message as Any), ("msgId", msgId as Any), ("savedId", savedId as Any), ("convertStars", convertStars as Any), ("upgradeStars", upgradeStars as Any), ("canExportAt", canExportAt as Any), ("transferStars", transferStars as Any), ("canTransferAt", canTransferAt as Any), ("canResellAt", canResellAt as Any), ("collectionId", collectionId as Any), ("prepaidUpgradeHash", prepaidUpgradeHash as Any), ("dropOriginalDetailsStars", dropOriginalDetailsStars as Any), ("giftNum", giftNum as Any)]) } } @@ -706,6 +707,8 @@ public extension Api { if Int(_1!) & Int(1 << 16) != 0 {_15 = parseString(reader) } var _16: Int64? if Int(_1!) & Int(1 << 18) != 0 {_16 = reader.readInt64() } + var _17: Int32? + if Int(_1!) & Int(1 << 19) != 0 {_17 = reader.readInt32() } let _c1 = _1 != nil let _c2 = (Int(_1!) & Int(1 << 1) == 0) || _2 != nil let _c3 = _3 != nil @@ -722,8 +725,9 @@ public extension Api { let _c14 = (Int(_1!) & Int(1 << 15) == 0) || _14 != nil let _c15 = (Int(_1!) & Int(1 << 16) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 18) == 0) || _16 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { - return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13, collectionId: _14, prepaidUpgradeHash: _15, dropOriginalDetailsStars: _16) + let _c17 = (Int(_1!) & Int(1 << 19) == 0) || _17 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 { + return Api.SavedStarGift.savedStarGift(flags: _1!, fromId: _2, date: _3!, gift: _4!, message: _5, msgId: _6, savedId: _7, convertStars: _8, upgradeStars: _9, canExportAt: _10, transferStars: _11, canTransferAt: _12, canResellAt: _13, collectionId: _14, prepaidUpgradeHash: _15, dropOriginalDetailsStars: _16, giftNum: _17) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 4baea64b9d..8b6e650407 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -288,14 +288,14 @@ public extension Api { } public extension Api { enum StarGift: TypeConstructorDescription { - case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?, auctionSlug: String?, giftsPerRound: Int32?) - case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, themePeer: Api.Peer?, peerColor: Api.PeerColor?, hostId: Api.Peer?, offerMinStars: Int32?) + case starGift(flags: Int32, id: Int64, sticker: Api.Document, stars: Int64, availabilityRemains: Int32?, availabilityTotal: Int32?, availabilityResale: Int64?, convertStars: Int64, firstSaleDate: Int32?, lastSaleDate: Int32?, upgradeStars: Int64?, resellMinStars: Int64?, title: String?, releasedBy: Api.Peer?, perUserTotal: Int32?, perUserRemains: Int32?, lockedUntilDate: Int32?, auctionSlug: String?, giftsPerRound: Int32?, auctionStartDate: Int32?, upgradeVariants: Int32?) + case starGiftUnique(flags: Int32, id: Int64, giftId: Int64, title: String, slug: String, num: Int32, ownerId: Api.Peer?, ownerName: String?, ownerAddress: String?, attributes: [Api.StarGiftAttribute], availabilityIssued: Int32, availabilityTotal: Int32, giftAddress: String?, resellAmount: [Api.StarsAmount]?, releasedBy: Api.Peer?, valueAmount: Int64?, valueCurrency: String?, valueUsdAmount: Int64?, themePeer: Api.Peer?, peerColor: Api.PeerColor?, hostId: Api.Peer?, offerMinStars: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound): + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound, let auctionStartDate, let upgradeVariants): if boxed { - buffer.appendInt32(463097215) + buffer.appendInt32(1453798502) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -316,10 +316,12 @@ public extension Api { if Int(flags) & Int(1 << 9) != 0 {serializeInt32(lockedUntilDate!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 11) != 0 {serializeString(auctionSlug!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 11) != 0 {serializeInt32(giftsPerRound!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 11) != 0 {serializeInt32(auctionStartDate!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 12) != 0 {serializeInt32(upgradeVariants!, buffer: buffer, boxed: false)} break - case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId, let offerMinStars): + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let valueUsdAmount, let themePeer, let peerColor, let hostId, let offerMinStars): if boxed { - buffer.appendInt32(-1624264554) + buffer.appendInt32(1453155529) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(id, buffer: buffer, boxed: false) @@ -346,6 +348,7 @@ public extension Api { if Int(flags) & Int(1 << 5) != 0 {releasedBy!.serialize(buffer, true)} if Int(flags) & Int(1 << 8) != 0 {serializeInt64(valueAmount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 8) != 0 {serializeString(valueCurrency!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 8) != 0 {serializeInt64(valueUsdAmount!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 10) != 0 {themePeer!.serialize(buffer, true)} if Int(flags) & Int(1 << 11) != 0 {peerColor!.serialize(buffer, true)} if Int(flags) & Int(1 << 12) != 0 {hostId!.serialize(buffer, true)} @@ -356,10 +359,10 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound): - return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any), ("auctionSlug", auctionSlug as Any), ("giftsPerRound", giftsPerRound as Any)]) - case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let themePeer, let peerColor, let hostId, let offerMinStars): - return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("themePeer", themePeer as Any), ("peerColor", peerColor as Any), ("hostId", hostId as Any), ("offerMinStars", offerMinStars as Any)]) + case .starGift(let flags, let id, let sticker, let stars, let availabilityRemains, let availabilityTotal, let availabilityResale, let convertStars, let firstSaleDate, let lastSaleDate, let upgradeStars, let resellMinStars, let title, let releasedBy, let perUserTotal, let perUserRemains, let lockedUntilDate, let auctionSlug, let giftsPerRound, let auctionStartDate, let upgradeVariants): + return ("starGift", [("flags", flags as Any), ("id", id as Any), ("sticker", sticker as Any), ("stars", stars as Any), ("availabilityRemains", availabilityRemains as Any), ("availabilityTotal", availabilityTotal as Any), ("availabilityResale", availabilityResale as Any), ("convertStars", convertStars as Any), ("firstSaleDate", firstSaleDate as Any), ("lastSaleDate", lastSaleDate as Any), ("upgradeStars", upgradeStars as Any), ("resellMinStars", resellMinStars as Any), ("title", title as Any), ("releasedBy", releasedBy as Any), ("perUserTotal", perUserTotal as Any), ("perUserRemains", perUserRemains as Any), ("lockedUntilDate", lockedUntilDate as Any), ("auctionSlug", auctionSlug as Any), ("giftsPerRound", giftsPerRound as Any), ("auctionStartDate", auctionStartDate as Any), ("upgradeVariants", upgradeVariants as Any)]) + case .starGiftUnique(let flags, let id, let giftId, let title, let slug, let num, let ownerId, let ownerName, let ownerAddress, let attributes, let availabilityIssued, let availabilityTotal, let giftAddress, let resellAmount, let releasedBy, let valueAmount, let valueCurrency, let valueUsdAmount, let themePeer, let peerColor, let hostId, let offerMinStars): + return ("starGiftUnique", [("flags", flags as Any), ("id", id as Any), ("giftId", giftId as Any), ("title", title as Any), ("slug", slug as Any), ("num", num as Any), ("ownerId", ownerId as Any), ("ownerName", ownerName as Any), ("ownerAddress", ownerAddress as Any), ("attributes", attributes as Any), ("availabilityIssued", availabilityIssued as Any), ("availabilityTotal", availabilityTotal as Any), ("giftAddress", giftAddress as Any), ("resellAmount", resellAmount as Any), ("releasedBy", releasedBy as Any), ("valueAmount", valueAmount as Any), ("valueCurrency", valueCurrency as Any), ("valueUsdAmount", valueUsdAmount as Any), ("themePeer", themePeer as Any), ("peerColor", peerColor as Any), ("hostId", hostId as Any), ("offerMinStars", offerMinStars as Any)]) } } @@ -406,6 +409,10 @@ public extension Api { if Int(_1!) & Int(1 << 11) != 0 {_18 = parseString(reader) } var _19: Int32? if Int(_1!) & Int(1 << 11) != 0 {_19 = reader.readInt32() } + var _20: Int32? + if Int(_1!) & Int(1 << 11) != 0 {_20 = reader.readInt32() } + var _21: Int32? + if Int(_1!) & Int(1 << 12) != 0 {_21 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -425,8 +432,10 @@ public extension Api { let _c17 = (Int(_1!) & Int(1 << 9) == 0) || _17 != nil let _c18 = (Int(_1!) & Int(1 << 11) == 0) || _18 != nil let _c19 = (Int(_1!) & Int(1 << 11) == 0) || _19 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 { - return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16, lockedUntilDate: _17, auctionSlug: _18, giftsPerRound: _19) + let _c20 = (Int(_1!) & Int(1 << 11) == 0) || _20 != nil + let _c21 = (Int(_1!) & Int(1 << 12) == 0) || _21 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 { + return Api.StarGift.starGift(flags: _1!, id: _2!, sticker: _3!, stars: _4!, availabilityRemains: _5, availabilityTotal: _6, availabilityResale: _7, convertStars: _8!, firstSaleDate: _9, lastSaleDate: _10, upgradeStars: _11, resellMinStars: _12, title: _13, releasedBy: _14, perUserTotal: _15, perUserRemains: _16, lockedUntilDate: _17, auctionSlug: _18, giftsPerRound: _19, auctionStartDate: _20, upgradeVariants: _21) } else { return nil @@ -475,20 +484,22 @@ public extension Api { if Int(_1!) & Int(1 << 8) != 0 {_16 = reader.readInt64() } var _17: String? if Int(_1!) & Int(1 << 8) != 0 {_17 = parseString(reader) } - var _18: Api.Peer? + var _18: Int64? + if Int(_1!) & Int(1 << 8) != 0 {_18 = reader.readInt64() } + var _19: Api.Peer? if Int(_1!) & Int(1 << 10) != 0 {if let signature = reader.readInt32() { - _18 = Api.parse(reader, signature: signature) as? Api.Peer + _19 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _19: Api.PeerColor? + var _20: Api.PeerColor? if Int(_1!) & Int(1 << 11) != 0 {if let signature = reader.readInt32() { - _19 = Api.parse(reader, signature: signature) as? Api.PeerColor + _20 = Api.parse(reader, signature: signature) as? Api.PeerColor } } - var _20: Api.Peer? + var _21: Api.Peer? if Int(_1!) & Int(1 << 12) != 0 {if let signature = reader.readInt32() { - _20 = Api.parse(reader, signature: signature) as? Api.Peer + _21 = Api.parse(reader, signature: signature) as? Api.Peer } } - var _21: Int32? - if Int(_1!) & Int(1 << 13) != 0 {_21 = reader.readInt32() } + var _22: Int32? + if Int(_1!) & Int(1 << 13) != 0 {_22 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -506,12 +517,13 @@ public extension Api { let _c15 = (Int(_1!) & Int(1 << 5) == 0) || _15 != nil let _c16 = (Int(_1!) & Int(1 << 8) == 0) || _16 != nil let _c17 = (Int(_1!) & Int(1 << 8) == 0) || _17 != nil - let _c18 = (Int(_1!) & Int(1 << 10) == 0) || _18 != nil - let _c19 = (Int(_1!) & Int(1 << 11) == 0) || _19 != nil - let _c20 = (Int(_1!) & Int(1 << 12) == 0) || _20 != nil - let _c21 = (Int(_1!) & Int(1 << 13) == 0) || _21 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 { - return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, themePeer: _18, peerColor: _19, hostId: _20, offerMinStars: _21) + let _c18 = (Int(_1!) & Int(1 << 8) == 0) || _18 != nil + let _c19 = (Int(_1!) & Int(1 << 10) == 0) || _19 != nil + let _c20 = (Int(_1!) & Int(1 << 11) == 0) || _20 != nil + let _c21 = (Int(_1!) & Int(1 << 12) == 0) || _21 != nil + let _c22 = (Int(_1!) & Int(1 << 13) == 0) || _22 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 && _c18 && _c19 && _c20 && _c21 && _c22 { + return Api.StarGift.starGiftUnique(flags: _1!, id: _2!, giftId: _3!, title: _4!, slug: _5!, num: _6!, ownerId: _7, ownerName: _8, ownerAddress: _9, attributes: _10!, availabilityIssued: _11!, availabilityTotal: _12!, giftAddress: _13, resellAmount: _14, releasedBy: _15, valueAmount: _16, valueCurrency: _17, valueUsdAmount: _18, themePeer: _19, peerColor: _20, hostId: _21, offerMinStars: _22) } else { return nil @@ -852,13 +864,13 @@ public extension Api { } public extension Api { enum StarGiftAuctionAcquiredGift: TypeConstructorDescription { - case starGiftAuctionAcquiredGift(flags: Int32, peer: Api.Peer, date: Int32, bidAmount: Int64, round: Int32, pos: Int32, message: Api.TextWithEntities?) + case starGiftAuctionAcquiredGift(flags: Int32, peer: Api.Peer, date: Int32, bidAmount: Int64, round: Int32, pos: Int32, message: Api.TextWithEntities?, giftNum: Int32?) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message): + case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message, let giftNum): if boxed { - buffer.appendInt32(-1419714037) + buffer.appendInt32(1118831432) } serializeInt32(flags, buffer: buffer, boxed: false) peer.serialize(buffer, true) @@ -867,14 +879,15 @@ public extension Api { serializeInt32(round, buffer: buffer, boxed: false) serializeInt32(pos, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 1) != 0 {message!.serialize(buffer, true)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(giftNum!, buffer: buffer, boxed: false)} break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message): - return ("starGiftAuctionAcquiredGift", [("flags", flags as Any), ("peer", peer as Any), ("date", date as Any), ("bidAmount", bidAmount as Any), ("round", round as Any), ("pos", pos as Any), ("message", message as Any)]) + case .starGiftAuctionAcquiredGift(let flags, let peer, let date, let bidAmount, let round, let pos, let message, let giftNum): + return ("starGiftAuctionAcquiredGift", [("flags", flags as Any), ("peer", peer as Any), ("date", date as Any), ("bidAmount", bidAmount as Any), ("round", round as Any), ("pos", pos as Any), ("message", message as Any), ("giftNum", giftNum as Any)]) } } @@ -897,6 +910,8 @@ public extension Api { if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { _7 = Api.parse(reader, signature: signature) as? Api.TextWithEntities } } + var _8: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_8 = reader.readInt32() } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -904,8 +919,81 @@ public extension Api { let _c5 = _5 != nil let _c6 = _6 != nil let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 { - return Api.StarGiftAuctionAcquiredGift.starGiftAuctionAcquiredGift(flags: _1!, peer: _2!, date: _3!, bidAmount: _4!, round: _5!, pos: _6!, message: _7) + let _c8 = (Int(_1!) & Int(1 << 2) == 0) || _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.StarGiftAuctionAcquiredGift.starGiftAuctionAcquiredGift(flags: _1!, peer: _2!, date: _3!, bidAmount: _4!, round: _5!, pos: _6!, message: _7, giftNum: _8) + } + else { + return nil + } + } + + } +} +public extension Api { + enum StarGiftAuctionRound: TypeConstructorDescription { + case starGiftAuctionRound(num: Int32, duration: Int32) + case starGiftAuctionRoundExtendable(num: Int32, duration: Int32, extendTop: Int32, extendWindow: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starGiftAuctionRound(let num, let duration): + if boxed { + buffer.appendInt32(984483112) + } + serializeInt32(num, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + break + case .starGiftAuctionRoundExtendable(let num, let duration, let extendTop, let extendWindow): + if boxed { + buffer.appendInt32(178266597) + } + serializeInt32(num, buffer: buffer, boxed: false) + serializeInt32(duration, buffer: buffer, boxed: false) + serializeInt32(extendTop, buffer: buffer, boxed: false) + serializeInt32(extendWindow, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starGiftAuctionRound(let num, let duration): + return ("starGiftAuctionRound", [("num", num as Any), ("duration", duration as Any)]) + case .starGiftAuctionRoundExtendable(let num, let duration, let extendTop, let extendWindow): + return ("starGiftAuctionRoundExtendable", [("num", num as Any), ("duration", duration as Any), ("extendTop", extendTop as Any), ("extendWindow", extendWindow as Any)]) + } + } + + public static func parse_starGiftAuctionRound(_ reader: BufferReader) -> StarGiftAuctionRound? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StarGiftAuctionRound.starGiftAuctionRound(num: _1!, duration: _2!) + } + else { + return nil + } + } + public static func parse_starGiftAuctionRoundExtendable(_ reader: BufferReader) -> StarGiftAuctionRound? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StarGiftAuctionRound.starGiftAuctionRoundExtendable(num: _1!, duration: _2!, extendTop: _3!, extendWindow: _4!) } else { return nil @@ -916,15 +1004,15 @@ public extension Api { } public extension Api { enum StarGiftAuctionState: TypeConstructorDescription { - case starGiftAuctionState(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [Api.AuctionBidLevel], topBidders: [Int64], nextRoundAt: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32) + case starGiftAuctionState(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [Api.AuctionBidLevel], topBidders: [Int64], nextRoundAt: Int32, lastGiftNum: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32, rounds: [Api.StarGiftAuctionRound]) case starGiftAuctionStateFinished(startDate: Int32, endDate: Int32, averagePrice: Int64) case starGiftAuctionStateNotModified public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let giftsLeft, let currentRound, let totalRounds): + case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let lastGiftNum, let giftsLeft, let currentRound, let totalRounds, let rounds): if boxed { - buffer.appendInt32(1571835723) + buffer.appendInt32(1998212710) } serializeInt32(version, buffer: buffer, boxed: false) serializeInt32(startDate, buffer: buffer, boxed: false) @@ -941,9 +1029,15 @@ public extension Api { serializeInt64(item, buffer: buffer, boxed: false) } serializeInt32(nextRoundAt, buffer: buffer, boxed: false) + serializeInt32(lastGiftNum, buffer: buffer, boxed: false) serializeInt32(giftsLeft, buffer: buffer, boxed: false) serializeInt32(currentRound, buffer: buffer, boxed: false) serializeInt32(totalRounds, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(rounds.count)) + for item in rounds { + item.serialize(buffer, true) + } break case .starGiftAuctionStateFinished(let startDate, let endDate, let averagePrice): if boxed { @@ -964,8 +1058,8 @@ public extension Api { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let giftsLeft, let currentRound, let totalRounds): - return ("starGiftAuctionState", [("version", version as Any), ("startDate", startDate as Any), ("endDate", endDate as Any), ("minBidAmount", minBidAmount as Any), ("bidLevels", bidLevels as Any), ("topBidders", topBidders as Any), ("nextRoundAt", nextRoundAt as Any), ("giftsLeft", giftsLeft as Any), ("currentRound", currentRound as Any), ("totalRounds", totalRounds as Any)]) + case .starGiftAuctionState(let version, let startDate, let endDate, let minBidAmount, let bidLevels, let topBidders, let nextRoundAt, let lastGiftNum, let giftsLeft, let currentRound, let totalRounds, let rounds): + return ("starGiftAuctionState", [("version", version as Any), ("startDate", startDate as Any), ("endDate", endDate as Any), ("minBidAmount", minBidAmount as Any), ("bidLevels", bidLevels as Any), ("topBidders", topBidders as Any), ("nextRoundAt", nextRoundAt as Any), ("lastGiftNum", lastGiftNum as Any), ("giftsLeft", giftsLeft as Any), ("currentRound", currentRound as Any), ("totalRounds", totalRounds as Any), ("rounds", rounds as Any)]) case .starGiftAuctionStateFinished(let startDate, let endDate, let averagePrice): return ("starGiftAuctionStateFinished", [("startDate", startDate as Any), ("endDate", endDate as Any), ("averagePrice", averagePrice as Any)]) case .starGiftAuctionStateNotModified: @@ -998,6 +1092,12 @@ public extension Api { _9 = reader.readInt32() var _10: Int32? _10 = reader.readInt32() + var _11: Int32? + _11 = reader.readInt32() + var _12: [Api.StarGiftAuctionRound]? + if let _ = reader.readInt32() { + _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAuctionRound.self) + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil @@ -1008,8 +1108,10 @@ public extension Api { let _c8 = _8 != nil let _c9 = _9 != nil let _c10 = _10 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 { - return Api.StarGiftAuctionState.starGiftAuctionState(version: _1!, startDate: _2!, endDate: _3!, minBidAmount: _4!, bidLevels: _5!, topBidders: _6!, nextRoundAt: _7!, giftsLeft: _8!, currentRound: _9!, totalRounds: _10!) + let _c11 = _11 != nil + let _c12 = _12 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 { + return Api.StarGiftAuctionState.starGiftAuctionState(version: _1!, startDate: _2!, endDate: _3!, minBidAmount: _4!, bidLevels: _5!, topBidders: _6!, nextRoundAt: _7!, lastGiftNum: _8!, giftsLeft: _9!, currentRound: _10!, totalRounds: _11!, rounds: _12!) } else { return nil @@ -1526,135 +1628,3 @@ public extension Api { } } -public extension Api { - enum StarsRevenueStatus: TypeConstructorDescription { - case starsRevenueStatus(flags: Int32, currentBalance: Api.StarsAmount, availableBalance: Api.StarsAmount, overallRevenue: Api.StarsAmount, nextWithdrawalAt: Int32?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt): - if boxed { - buffer.appendInt32(-21080943) - } - serializeInt32(flags, buffer: buffer, boxed: false) - currentBalance.serialize(buffer, true) - availableBalance.serialize(buffer, true) - overallRevenue.serialize(buffer, true) - if Int(flags) & Int(1 << 1) != 0 {serializeInt32(nextWithdrawalAt!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt): - return ("starsRevenueStatus", [("flags", flags as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any), ("nextWithdrawalAt", nextWithdrawalAt as Any)]) - } - } - - public static func parse_starsRevenueStatus(_ reader: BufferReader) -> StarsRevenueStatus? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.StarsAmount? - if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.StarsAmount - } - var _3: Api.StarsAmount? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount - } - var _4: Api.StarsAmount? - if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.StarsAmount - } - var _5: Int32? - if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.StarsRevenueStatus.starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!, nextWithdrawalAt: _5) - } - else { - return nil - } - } - - } -} -public extension Api { - enum StarsSubscription: TypeConstructorDescription { - case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing, chatInviteHash: String?, title: String?, photo: Api.WebDocument?, invoiceSlug: String?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug): - if boxed { - buffer.appendInt32(779004698) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(id, buffer: buffer, boxed: false) - peer.serialize(buffer, true) - serializeInt32(untilDate, buffer: buffer, boxed: false) - pricing.serialize(buffer, true) - if Int(flags) & Int(1 << 3) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 4) != 0 {serializeString(title!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)} - if Int(flags) & Int(1 << 6) != 0 {serializeString(invoiceSlug!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug): - return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any), ("chatInviteHash", chatInviteHash as Any), ("title", title as Any), ("photo", photo as Any), ("invoiceSlug", invoiceSlug as Any)]) - } - } - - public static func parse_starsSubscription(_ reader: BufferReader) -> StarsSubscription? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Api.Peer? - if let signature = reader.readInt32() { - _3 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _4: Int32? - _4 = reader.readInt32() - var _5: Api.StarsSubscriptionPricing? - if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing - } - var _6: String? - if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } - var _7: String? - if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } - var _8: Api.WebDocument? - if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { - _8 = Api.parse(reader, signature: signature) as? Api.WebDocument - } } - var _9: String? - if Int(_1!) & Int(1 << 6) != 0 {_9 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil - let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil - let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { - return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6, title: _7, photo: _8, invoiceSlug: _9) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index c6db21d4e0..aa293b5f49 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -1,3 +1,135 @@ +public extension Api { + enum StarsRevenueStatus: TypeConstructorDescription { + case starsRevenueStatus(flags: Int32, currentBalance: Api.StarsAmount, availableBalance: Api.StarsAmount, overallRevenue: Api.StarsAmount, nextWithdrawalAt: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt): + if boxed { + buffer.appendInt32(-21080943) + } + serializeInt32(flags, buffer: buffer, boxed: false) + currentBalance.serialize(buffer, true) + availableBalance.serialize(buffer, true) + overallRevenue.serialize(buffer, true) + if Int(flags) & Int(1 << 1) != 0 {serializeInt32(nextWithdrawalAt!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsRevenueStatus(let flags, let currentBalance, let availableBalance, let overallRevenue, let nextWithdrawalAt): + return ("starsRevenueStatus", [("flags", flags as Any), ("currentBalance", currentBalance as Any), ("availableBalance", availableBalance as Any), ("overallRevenue", overallRevenue as Any), ("nextWithdrawalAt", nextWithdrawalAt as Any)]) + } + } + + public static func parse_starsRevenueStatus(_ reader: BufferReader) -> StarsRevenueStatus? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.StarsAmount? + if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.StarsAmount + } + var _3: Api.StarsAmount? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.StarsAmount + } + var _4: Api.StarsAmount? + if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.StarsAmount + } + var _5: Int32? + if Int(_1!) & Int(1 << 1) != 0 {_5 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = (Int(_1!) & Int(1 << 1) == 0) || _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.StarsRevenueStatus.starsRevenueStatus(flags: _1!, currentBalance: _2!, availableBalance: _3!, overallRevenue: _4!, nextWithdrawalAt: _5) + } + else { + return nil + } + } + + } +} +public extension Api { + enum StarsSubscription: TypeConstructorDescription { + case starsSubscription(flags: Int32, id: String, peer: Api.Peer, untilDate: Int32, pricing: Api.StarsSubscriptionPricing, chatInviteHash: String?, title: String?, photo: Api.WebDocument?, invoiceSlug: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug): + if boxed { + buffer.appendInt32(779004698) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(id, buffer: buffer, boxed: false) + peer.serialize(buffer, true) + serializeInt32(untilDate, buffer: buffer, boxed: false) + pricing.serialize(buffer, true) + if Int(flags) & Int(1 << 3) != 0 {serializeString(chatInviteHash!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 4) != 0 {serializeString(title!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 5) != 0 {photo!.serialize(buffer, true)} + if Int(flags) & Int(1 << 6) != 0 {serializeString(invoiceSlug!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starsSubscription(let flags, let id, let peer, let untilDate, let pricing, let chatInviteHash, let title, let photo, let invoiceSlug): + return ("starsSubscription", [("flags", flags as Any), ("id", id as Any), ("peer", peer as Any), ("untilDate", untilDate as Any), ("pricing", pricing as Any), ("chatInviteHash", chatInviteHash as Any), ("title", title as Any), ("photo", photo as Any), ("invoiceSlug", invoiceSlug as Any)]) + } + } + + public static func parse_starsSubscription(_ reader: BufferReader) -> StarsSubscription? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Api.Peer? + if let signature = reader.readInt32() { + _3 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _4: Int32? + _4 = reader.readInt32() + var _5: Api.StarsSubscriptionPricing? + if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.StarsSubscriptionPricing + } + var _6: String? + if Int(_1!) & Int(1 << 3) != 0 {_6 = parseString(reader) } + var _7: String? + if Int(_1!) & Int(1 << 4) != 0 {_7 = parseString(reader) } + var _8: Api.WebDocument? + if Int(_1!) & Int(1 << 5) != 0 {if let signature = reader.readInt32() { + _8 = Api.parse(reader, signature: signature) as? Api.WebDocument + } } + var _9: String? + if Int(_1!) & Int(1 << 6) != 0 {_9 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 3) == 0) || _6 != nil + let _c7 = (Int(_1!) & Int(1 << 4) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 5) == 0) || _8 != nil + let _c9 = (Int(_1!) & Int(1 << 6) == 0) || _9 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 { + return Api.StarsSubscription.starsSubscription(flags: _1!, id: _2!, peer: _3!, untilDate: _4!, pricing: _5!, chatInviteHash: _6, title: _7, photo: _8, invoiceSlug: _9) + } + else { + return nil + } + } + + } +} public extension Api { enum StarsSubscriptionPricing: TypeConstructorDescription { case starsSubscriptionPricing(period: Int32, amount: Int64) @@ -1160,227 +1292,3 @@ public extension Api { } } -public extension Api { - enum StoryFwdHeader: TypeConstructorDescription { - case storyFwdHeader(flags: Int32, from: Api.Peer?, fromName: String?, storyId: Int32?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyFwdHeader(let flags, let from, let fromName, let storyId): - if boxed { - buffer.appendInt32(-1205411504) - } - serializeInt32(flags, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {from!.serialize(buffer, true)} - if Int(flags) & Int(1 << 1) != 0 {serializeString(fromName!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 2) != 0 {serializeInt32(storyId!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyFwdHeader(let flags, let from, let fromName, let storyId): - return ("storyFwdHeader", [("flags", flags as Any), ("from", from as Any), ("fromName", fromName as Any), ("storyId", storyId as Any)]) - } - } - - public static func parse_storyFwdHeader(_ reader: BufferReader) -> StoryFwdHeader? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Api.Peer? - if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _2 = Api.parse(reader, signature: signature) as? Api.Peer - } } - var _3: String? - if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } - var _4: Int32? - if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } - let _c1 = _1 != nil - let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil - let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil - let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.StoryFwdHeader.storyFwdHeader(flags: _1!, from: _2, fromName: _3, storyId: _4) - } - else { - return nil - } - } - - } -} -public extension Api { - indirect enum StoryItem: TypeConstructorDescription { - case storyItem(flags: Int32, id: Int32, date: Int32, fromId: Api.Peer?, fwdFrom: Api.StoryFwdHeader?, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?, albums: [Int32]?) - case storyItemDeleted(id: Int32) - case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums): - if boxed { - buffer.appendInt32(-302947087) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(id, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 18) != 0 {fromId!.serialize(buffer, true)} - if Int(flags) & Int(1 << 17) != 0 {fwdFrom!.serialize(buffer, true)} - serializeInt32(expireDate, buffer: buffer, boxed: false) - if Int(flags) & Int(1 << 0) != 0 {serializeString(caption!, buffer: buffer, boxed: false)} - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(entities!.count)) - for item in entities! { - item.serialize(buffer, true) - }} - media.serialize(buffer, true) - if Int(flags) & Int(1 << 14) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(mediaAreas!.count)) - for item in mediaAreas! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(privacy!.count)) - for item in privacy! { - item.serialize(buffer, true) - }} - if Int(flags) & Int(1 << 3) != 0 {views!.serialize(buffer, true)} - if Int(flags) & Int(1 << 15) != 0 {sentReaction!.serialize(buffer, true)} - if Int(flags) & Int(1 << 19) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(albums!.count)) - for item in albums! { - serializeInt32(item, buffer: buffer, boxed: false) - }} - break - case .storyItemDeleted(let id): - if boxed { - buffer.appendInt32(1374088783) - } - serializeInt32(id, buffer: buffer, boxed: false) - break - case .storyItemSkipped(let flags, let id, let date, let expireDate): - if boxed { - buffer.appendInt32(-5388013) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(id, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(expireDate, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums): - return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("fromId", fromId as Any), ("fwdFrom", fwdFrom as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any), ("albums", albums as Any)]) - case .storyItemDeleted(let id): - return ("storyItemDeleted", [("id", id as Any)]) - case .storyItemSkipped(let flags, let id, let date, let expireDate): - return ("storyItemSkipped", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any)]) - } - } - - public static func parse_storyItem(_ reader: BufferReader) -> StoryItem? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Api.Peer? - if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() { - _4 = Api.parse(reader, signature: signature) as? Api.Peer - } } - var _5: Api.StoryFwdHeader? - if Int(_1!) & Int(1 << 17) != 0 {if let signature = reader.readInt32() { - _5 = Api.parse(reader, signature: signature) as? Api.StoryFwdHeader - } } - var _6: Int32? - _6 = reader.readInt32() - var _7: String? - if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) } - var _8: [Api.MessageEntity]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) - } } - var _9: Api.MessageMedia? - if let signature = reader.readInt32() { - _9 = Api.parse(reader, signature: signature) as? Api.MessageMedia - } - var _10: [Api.MediaArea]? - if Int(_1!) & Int(1 << 14) != 0 {if let _ = reader.readInt32() { - _10 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MediaArea.self) - } } - var _11: [Api.PrivacyRule]? - if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() { - _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrivacyRule.self) - } } - var _12: Api.StoryViews? - if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { - _12 = Api.parse(reader, signature: signature) as? Api.StoryViews - } } - var _13: Api.Reaction? - if Int(_1!) & Int(1 << 15) != 0 {if let signature = reader.readInt32() { - _13 = Api.parse(reader, signature: signature) as? Api.Reaction - } } - var _14: [Int32]? - if Int(_1!) & Int(1 << 19) != 0 {if let _ = reader.readInt32() { - _14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) - } } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = (Int(_1!) & Int(1 << 18) == 0) || _4 != nil - let _c5 = (Int(_1!) & Int(1 << 17) == 0) || _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil - let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil - let _c9 = _9 != nil - let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil - let _c11 = (Int(_1!) & Int(1 << 2) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil - let _c14 = (Int(_1!) & Int(1 << 19) == 0) || _14 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { - return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, fromId: _4, fwdFrom: _5, expireDate: _6!, caption: _7, entities: _8, media: _9!, mediaAreas: _10, privacy: _11, views: _12, sentReaction: _13, albums: _14) - } - else { - return nil - } - } - public static func parse_storyItemDeleted(_ reader: BufferReader) -> StoryItem? { - var _1: Int32? - _1 = reader.readInt32() - let _c1 = _1 != nil - if _c1 { - return Api.StoryItem.storyItemDeleted(id: _1!) - } - else { - return nil - } - } - public static func parse_storyItemSkipped(_ reader: BufferReader) -> StoryItem? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - if _c1 && _c2 && _c3 && _c4 { - return Api.StoryItem.storyItemSkipped(flags: _1!, id: _2!, date: _3!, expireDate: _4!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api27.swift b/submodules/TelegramApi/Sources/Api27.swift index 9327244759..88079a7430 100644 --- a/submodules/TelegramApi/Sources/Api27.swift +++ b/submodules/TelegramApi/Sources/Api27.swift @@ -1,3 +1,227 @@ +public extension Api { + enum StoryFwdHeader: TypeConstructorDescription { + case storyFwdHeader(flags: Int32, from: Api.Peer?, fromName: String?, storyId: Int32?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyFwdHeader(let flags, let from, let fromName, let storyId): + if boxed { + buffer.appendInt32(-1205411504) + } + serializeInt32(flags, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {from!.serialize(buffer, true)} + if Int(flags) & Int(1 << 1) != 0 {serializeString(fromName!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 2) != 0 {serializeInt32(storyId!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyFwdHeader(let flags, let from, let fromName, let storyId): + return ("storyFwdHeader", [("flags", flags as Any), ("from", from as Any), ("fromName", fromName as Any), ("storyId", storyId as Any)]) + } + } + + public static func parse_storyFwdHeader(_ reader: BufferReader) -> StoryFwdHeader? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Api.Peer? + if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { + _2 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _3: String? + if Int(_1!) & Int(1 << 1) != 0 {_3 = parseString(reader) } + var _4: Int32? + if Int(_1!) & Int(1 << 2) != 0 {_4 = reader.readInt32() } + let _c1 = _1 != nil + let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil + let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil + let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryFwdHeader.storyFwdHeader(flags: _1!, from: _2, fromName: _3, storyId: _4) + } + else { + return nil + } + } + + } +} +public extension Api { + indirect enum StoryItem: TypeConstructorDescription { + case storyItem(flags: Int32, id: Int32, date: Int32, fromId: Api.Peer?, fwdFrom: Api.StoryFwdHeader?, expireDate: Int32, caption: String?, entities: [Api.MessageEntity]?, media: Api.MessageMedia, mediaAreas: [Api.MediaArea]?, privacy: [Api.PrivacyRule]?, views: Api.StoryViews?, sentReaction: Api.Reaction?, albums: [Int32]?) + case storyItemDeleted(id: Int32) + case storyItemSkipped(flags: Int32, id: Int32, date: Int32, expireDate: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums): + if boxed { + buffer.appendInt32(-302947087) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(id, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 18) != 0 {fromId!.serialize(buffer, true)} + if Int(flags) & Int(1 << 17) != 0 {fwdFrom!.serialize(buffer, true)} + serializeInt32(expireDate, buffer: buffer, boxed: false) + if Int(flags) & Int(1 << 0) != 0 {serializeString(caption!, buffer: buffer, boxed: false)} + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(entities!.count)) + for item in entities! { + item.serialize(buffer, true) + }} + media.serialize(buffer, true) + if Int(flags) & Int(1 << 14) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(mediaAreas!.count)) + for item in mediaAreas! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(privacy!.count)) + for item in privacy! { + item.serialize(buffer, true) + }} + if Int(flags) & Int(1 << 3) != 0 {views!.serialize(buffer, true)} + if Int(flags) & Int(1 << 15) != 0 {sentReaction!.serialize(buffer, true)} + if Int(flags) & Int(1 << 19) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(albums!.count)) + for item in albums! { + serializeInt32(item, buffer: buffer, boxed: false) + }} + break + case .storyItemDeleted(let id): + if boxed { + buffer.appendInt32(1374088783) + } + serializeInt32(id, buffer: buffer, boxed: false) + break + case .storyItemSkipped(let flags, let id, let date, let expireDate): + if boxed { + buffer.appendInt32(-5388013) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(id, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(expireDate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyItem(let flags, let id, let date, let fromId, let fwdFrom, let expireDate, let caption, let entities, let media, let mediaAreas, let privacy, let views, let sentReaction, let albums): + return ("storyItem", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("fromId", fromId as Any), ("fwdFrom", fwdFrom as Any), ("expireDate", expireDate as Any), ("caption", caption as Any), ("entities", entities as Any), ("media", media as Any), ("mediaAreas", mediaAreas as Any), ("privacy", privacy as Any), ("views", views as Any), ("sentReaction", sentReaction as Any), ("albums", albums as Any)]) + case .storyItemDeleted(let id): + return ("storyItemDeleted", [("id", id as Any)]) + case .storyItemSkipped(let flags, let id, let date, let expireDate): + return ("storyItemSkipped", [("flags", flags as Any), ("id", id as Any), ("date", date as Any), ("expireDate", expireDate as Any)]) + } + } + + public static func parse_storyItem(_ reader: BufferReader) -> StoryItem? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Api.Peer? + if Int(_1!) & Int(1 << 18) != 0 {if let signature = reader.readInt32() { + _4 = Api.parse(reader, signature: signature) as? Api.Peer + } } + var _5: Api.StoryFwdHeader? + if Int(_1!) & Int(1 << 17) != 0 {if let signature = reader.readInt32() { + _5 = Api.parse(reader, signature: signature) as? Api.StoryFwdHeader + } } + var _6: Int32? + _6 = reader.readInt32() + var _7: String? + if Int(_1!) & Int(1 << 0) != 0 {_7 = parseString(reader) } + var _8: [Api.MessageEntity]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MessageEntity.self) + } } + var _9: Api.MessageMedia? + if let signature = reader.readInt32() { + _9 = Api.parse(reader, signature: signature) as? Api.MessageMedia + } + var _10: [Api.MediaArea]? + if Int(_1!) & Int(1 << 14) != 0 {if let _ = reader.readInt32() { + _10 = Api.parseVector(reader, elementSignature: 0, elementType: Api.MediaArea.self) + } } + var _11: [Api.PrivacyRule]? + if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() { + _11 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PrivacyRule.self) + } } + var _12: Api.StoryViews? + if Int(_1!) & Int(1 << 3) != 0 {if let signature = reader.readInt32() { + _12 = Api.parse(reader, signature: signature) as? Api.StoryViews + } } + var _13: Api.Reaction? + if Int(_1!) & Int(1 << 15) != 0 {if let signature = reader.readInt32() { + _13 = Api.parse(reader, signature: signature) as? Api.Reaction + } } + var _14: [Int32]? + if Int(_1!) & Int(1 << 19) != 0 {if let _ = reader.readInt32() { + _14 = Api.parseVector(reader, elementSignature: -1471112230, elementType: Int32.self) + } } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = (Int(_1!) & Int(1 << 18) == 0) || _4 != nil + let _c5 = (Int(_1!) & Int(1 << 17) == 0) || _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 0) == 0) || _7 != nil + let _c8 = (Int(_1!) & Int(1 << 1) == 0) || _8 != nil + let _c9 = _9 != nil + let _c10 = (Int(_1!) & Int(1 << 14) == 0) || _10 != nil + let _c11 = (Int(_1!) & Int(1 << 2) == 0) || _11 != nil + let _c12 = (Int(_1!) & Int(1 << 3) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 15) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 19) == 0) || _14 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { + return Api.StoryItem.storyItem(flags: _1!, id: _2!, date: _3!, fromId: _4, fwdFrom: _5, expireDate: _6!, caption: _7, entities: _8, media: _9!, mediaAreas: _10, privacy: _11, views: _12, sentReaction: _13, albums: _14) + } + else { + return nil + } + } + public static func parse_storyItemDeleted(_ reader: BufferReader) -> StoryItem? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.StoryItem.storyItemDeleted(id: _1!) + } + else { + return nil + } + } + public static func parse_storyItemSkipped(_ reader: BufferReader) -> StoryItem? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + if _c1 && _c2 && _c3 && _c4 { + return Api.StoryItem.storyItemSkipped(flags: _1!, id: _2!, date: _3!, expireDate: _4!) + } + else { + return nil + } + } + + } +} public extension Api { indirect enum StoryReaction: TypeConstructorDescription { case storyReaction(peerId: Api.Peer, date: Int32, reaction: Api.Reaction) diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index a6764c57a5..22436f1ec2 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -278,14 +278,14 @@ public extension Api.payments { } public extension Api.payments { enum StarGiftActiveAuctions: TypeConstructorDescription { - case starGiftActiveAuctions(auctions: [Api.StarGiftActiveAuctionState], users: [Api.User]) + case starGiftActiveAuctions(auctions: [Api.StarGiftActiveAuctionState], users: [Api.User], chats: [Api.Chat]) case starGiftActiveAuctionsNotModified public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGiftActiveAuctions(let auctions, let users): + case .starGiftActiveAuctions(let auctions, let users, let chats): if boxed { - buffer.appendInt32(-1745778728) + buffer.appendInt32(-1359565892) } buffer.appendInt32(481674261) buffer.appendInt32(Int32(auctions.count)) @@ -297,6 +297,11 @@ public extension Api.payments { for item in users { item.serialize(buffer, true) } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } break case .starGiftActiveAuctionsNotModified: if boxed { @@ -309,8 +314,8 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGiftActiveAuctions(let auctions, let users): - return ("starGiftActiveAuctions", [("auctions", auctions as Any), ("users", users as Any)]) + case .starGiftActiveAuctions(let auctions, let users, let chats): + return ("starGiftActiveAuctions", [("auctions", auctions as Any), ("users", users as Any), ("chats", chats as Any)]) case .starGiftActiveAuctionsNotModified: return ("starGiftActiveAuctionsNotModified", []) } @@ -325,10 +330,15 @@ public extension Api.payments { if let _ = reader.readInt32() { _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } + var _3: [Api.Chat]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.payments.StarGiftActiveAuctions.starGiftActiveAuctions(auctions: _1!, users: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.payments.StarGiftActiveAuctions.starGiftActiveAuctions(auctions: _1!, users: _2!, chats: _3!) } else { return nil @@ -404,13 +414,13 @@ public extension Api.payments { } public extension Api.payments { enum StarGiftAuctionState: TypeConstructorDescription { - case starGiftAuctionState(gift: Api.StarGift, state: Api.StarGiftAuctionState, userState: Api.StarGiftAuctionUserState, timeout: Int32, users: [Api.User]) + case starGiftAuctionState(gift: Api.StarGift, state: Api.StarGiftAuctionState, userState: Api.StarGiftAuctionUserState, timeout: Int32, users: [Api.User], chats: [Api.Chat]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users): + case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users, let chats): if boxed { - buffer.appendInt32(244900980) + buffer.appendInt32(1798960364) } gift.serialize(buffer, true) state.serialize(buffer, true) @@ -421,14 +431,19 @@ public extension Api.payments { for item in users { item.serialize(buffer, true) } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } break } } public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users): - return ("starGiftAuctionState", [("gift", gift as Any), ("state", state as Any), ("userState", userState as Any), ("timeout", timeout as Any), ("users", users as Any)]) + case .starGiftAuctionState(let gift, let state, let userState, let timeout, let users, let chats): + return ("starGiftAuctionState", [("gift", gift as Any), ("state", state as Any), ("userState", userState as Any), ("timeout", timeout as Any), ("users", users as Any), ("chats", chats as Any)]) } } @@ -451,13 +466,18 @@ public extension Api.payments { if let _ = reader.readInt32() { _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } + var _6: [Api.Chat]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } let _c1 = _1 != nil let _c2 = _2 != nil let _c3 = _3 != nil let _c4 = _4 != nil let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.payments.StarGiftAuctionState.starGiftAuctionState(gift: _1!, state: _2!, userState: _3!, timeout: _4!, users: _5!) + let _c6 = _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.payments.StarGiftAuctionState.starGiftAuctionState(gift: _1!, state: _2!, userState: _3!, timeout: _4!, users: _5!, chats: _6!) } else { return nil @@ -520,6 +540,48 @@ public extension Api.payments { } } +public extension Api.payments { + enum StarGiftUpgradeAttributes: TypeConstructorDescription { + case starGiftUpgradeAttributes(attributes: [Api.StarGiftAttribute]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .starGiftUpgradeAttributes(let attributes): + if boxed { + buffer.appendInt32(1187439471) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(attributes.count)) + for item in attributes { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .starGiftUpgradeAttributes(let attributes): + return ("starGiftUpgradeAttributes", [("attributes", attributes as Any)]) + } + } + + public static func parse_starGiftUpgradeAttributes(_ reader: BufferReader) -> StarGiftUpgradeAttributes? { + var _1: [Api.StarGiftAttribute]? + if let _ = reader.readInt32() { + _1 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StarGiftAttribute.self) + } + let _c1 = _1 != nil + if _c1 { + return Api.payments.StarGiftUpgradeAttributes.starGiftUpgradeAttributes(attributes: _1!) + } + else { + return nil + } + } + + } +} public extension Api.payments { enum StarGiftUpgradePreview: TypeConstructorDescription { case starGiftUpgradePreview(sampleAttributes: [Api.StarGiftAttribute], prices: [Api.StarGiftUpgradePrice], nextPrices: [Api.StarGiftUpgradePrice]) @@ -1616,51 +1678,3 @@ public extension Api.phone { } } -public extension Api.photos { - enum Photo: TypeConstructorDescription { - case photo(photo: Api.Photo, users: [Api.User]) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photo(let photo, let users): - if boxed { - buffer.appendInt32(539045032) - } - photo.serialize(buffer, true) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .photo(let photo, let users): - return ("photo", [("photo", photo as Any), ("users", users as Any)]) - } - } - - public static func parse_photo(_ reader: BufferReader) -> Photo? { - var _1: Api.Photo? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Photo - } - var _2: [Api.User]? - if let _ = reader.readInt32() { - _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.photos.Photo.photo(photo: _1!, users: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index 74ec103224..3b5d24155d 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,51 @@ +public extension Api.photos { + enum Photo: TypeConstructorDescription { + case photo(photo: Api.Photo, users: [Api.User]) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photo(let photo, let users): + if boxed { + buffer.appendInt32(539045032) + } + photo.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .photo(let photo, let users): + return ("photo", [("photo", photo as Any), ("users", users as Any)]) + } + } + + public static func parse_photo(_ reader: BufferReader) -> Photo? { + var _1: Api.Photo? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Photo + } + var _2: [Api.User]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.photos.Photo.photo(photo: _1!, users: _2!) + } + else { + return nil + } + } + + } +} public extension Api.photos { enum Photos: TypeConstructorDescription { case photos(photos: [Api.Photo], users: [Api.User]) @@ -1474,77 +1522,3 @@ public extension Api.stories { } } -public extension Api.stories { - enum StoryReactionsList: TypeConstructorDescription { - case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): - if boxed { - buffer.appendInt32(-1436583780) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(reactions.count)) - for item in reactions { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(chats.count)) - for item in chats { - item.serialize(buffer, true) - } - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(users.count)) - for item in users { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): - return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) - } - } - - public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: [Api.StoryReaction]? - if let _ = reader.readInt32() { - _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self) - } - var _4: [Api.Chat]? - if let _ = reader.readInt32() { - _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) - } - var _5: [Api.User]? - if let _ = reader.readInt32() { - _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) - } - var _6: String? - if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { - return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index f5fe6ba904..77f635e8bb 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -1,3 +1,77 @@ +public extension Api.stories { + enum StoryReactionsList: TypeConstructorDescription { + case storyReactionsList(flags: Int32, count: Int32, reactions: [Api.StoryReaction], chats: [Api.Chat], users: [Api.User], nextOffset: String?) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + if boxed { + buffer.appendInt32(-1436583780) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(reactions.count)) + for item in reactions { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(chats.count)) + for item in chats { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(users.count)) + for item in users { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 0) != 0 {serializeString(nextOffset!, buffer: buffer, boxed: false)} + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .storyReactionsList(let flags, let count, let reactions, let chats, let users, let nextOffset): + return ("storyReactionsList", [("flags", flags as Any), ("count", count as Any), ("reactions", reactions as Any), ("chats", chats as Any), ("users", users as Any), ("nextOffset", nextOffset as Any)]) + } + } + + public static func parse_storyReactionsList(_ reader: BufferReader) -> StoryReactionsList? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: [Api.StoryReaction]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StoryReaction.self) + } + var _4: [Api.Chat]? + if let _ = reader.readInt32() { + _4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self) + } + var _5: [Api.User]? + if let _ = reader.readInt32() { + _5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + } + var _6: String? + if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 { + return Api.stories.StoryReactionsList.storyReactionsList(flags: _1!, count: _2!, reactions: _3!, chats: _4!, users: _5!, nextOffset: _6) + } + else { + return nil + } + } + + } +} public extension Api.stories { enum StoryViews: TypeConstructorDescription { case storyViews(views: [Api.StoryViews], users: [Api.User]) diff --git a/submodules/TelegramApi/Sources/Api39.swift b/submodules/TelegramApi/Sources/Api39.swift index a5ed9e4f95..d421b4d691 100644 --- a/submodules/TelegramApi/Sources/Api39.swift +++ b/submodules/TelegramApi/Sources/Api39.swift @@ -9911,6 +9911,21 @@ public extension Api.functions.payments { }) } } +public extension Api.functions.payments { + static func getStarGiftUpgradeAttributes(giftId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(1828948824) + serializeInt64(giftId, buffer: buffer, boxed: false) + return (FunctionDescription(name: "payments.getStarGiftUpgradeAttributes", parameters: [("giftId", String(describing: giftId))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.payments.StarGiftUpgradeAttributes? in + let reader = BufferReader(buffer) + var result: Api.payments.StarGiftUpgradeAttributes? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.payments.StarGiftUpgradeAttributes + } + return result + }) + } +} public extension Api.functions.payments { static func getStarGiftUpgradePreview(giftId: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift index 94a6c208e1..4be03f3cb7 100644 --- a/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift +++ b/submodules/TelegramBaseController/Sources/GiftAuctionAccessoryPanel.swift @@ -103,8 +103,9 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode { } } - let titleText: String = self.strings.ChatList_Auctions_ActiveAuction(Int32(self.states.count)) - titleItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor)))))) + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + + var titleText: String = self.strings.ChatList_Auctions_ActiveAuction(Int32(self.states.count)) let subtitleText: String var subtitleTextColor = self.theme.rootController.navigationBar.secondaryTextColor @@ -134,13 +135,20 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode { subtitleText = self.strings.ChatList_Auctions_Status_Single_Winning(placeText).string } - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var startTime = currentTime var endTime = currentTime - if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _) = auctionState.auctionState { + if case let .ongoing(_, startDate, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState.auctionState { + startTime = startDate endTime = nextRoundDate } - let endTimeout = max(0, endTime - currentTime) + let endTimeout: Int32 + if currentTime < startTime { + endTimeout = max(0, startTime - currentTime) + titleText = self.strings.ChatList_Auctions_UpcomingAuction + } else { + endTimeout = max(0, endTime - currentTime) + } let hours = Int(endTimeout / 3600) let minutes = Int((endTimeout % 3600) / 60) @@ -179,6 +187,8 @@ final class GiftAuctionAccessoryPanel: ASDisplayNode { buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "view", content: .text(self.strings.ChatList_Auctions_View))) } + titleItems.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: titleText, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor)))))) + let buttonSize = self.button.update( transition: .spring(duration: 0.2), component: AnyComponent( diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index 9d11d920a3..e92c28725d 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -181,7 +181,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId)) case let .messageActionPrizeStars(flags, stars, transactionId, boostPeer, giveawayMsgId): return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId))) - case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash, giftMessageId, toId): + case let .messageActionStarGift(flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash, giftMessageId, toId, number): let text: String? let entities: [MessageTextEntity]? switch message { @@ -195,7 +195,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe guard let gift = StarGift(apiStarGift: apiGift) else { return nil } - return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0, isAuctionAcquired: (flags & (1 << 17)) != 0, toPeerId: toId?.peerId)) + return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0, isAuctionAcquired: (flags & (1 << 17)) != 0, toPeerId: toId?.peerId, number: number)) case let .messageActionStarGiftUnique(flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars): guard let gift = StarGift(apiStarGift: apiGift) else { return nil diff --git a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift index 0413196f78..4a954b1c76 100644 --- a/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift +++ b/submodules/TelegramCore/Sources/State/ManagedRecentStickers.swift @@ -360,6 +360,7 @@ func managedUniqueStarGifts(accountPeerId: PeerId, postbox: Postbox, network: Ne releasedBy: nil, valueAmount: nil, valueCurrency: nil, + valueUsdAmount: nil, flags: [], themePeerId: nil, peerColor: nil, diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index c81fbbf369..b6a82d2f10 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -148,6 +148,7 @@ public struct Namespaces { public static let cachedProfileSavedMusic: Int8 = 49 public static let cachedChatThemes: Int8 = 50 public static let cachedLiveStorySendAsPeers: Int8 = 51 + public static let cachedGiftUpgradesAttributes: Int8 = 52 } public struct UnorderedItemList { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 82dcbc27a4..f65aaeded5 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -255,7 +255,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentRefunded(peerId: PeerId, currency: String, totalAmount: Int64, payload: Data?, transactionId: String) case giftStars(currency: String, amount: Int64, count: Int64, cryptoCurrency: String?, cryptoAmount: Int64?, transactionId: String?) case prizeStars(amount: Int64, isUnclaimed: Bool, boostPeerId: PeerId?, transactionId: String?, giveawayMessageId: MessageId?) - case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?) + case starGift(gift: StarGift, convertStars: Int64?, text: String?, entities: [MessageTextEntity]?, nameHidden: Bool, savedToProfile: Bool, converted: Bool, upgraded: Bool, canUpgrade: Bool, upgradeStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, upgradeMessageId: Int32?, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, prepaidUpgradeHash: String?, giftMessageId: Int32?, upgradeSeparate: Bool, isAuctionAcquired: Bool, toPeerId: EnginePeer.Id?, number: Int32?) case starGiftUnique(gift: StarGift, isUpgrade: Bool, isTransferred: Bool, savedToProfile: Bool, canExportDate: Int32?, transferStars: Int64?, isRefunded: Bool, isPrepaidUpgrade: Bool, peerId: EnginePeer.Id?, senderId: EnginePeer.Id?, savedId: Int64?, resaleAmount: CurrencyAmount?, canTransferDate: Int32?, canResaleDate: Int32?, dropOriginalDetailsStars: Int64?, assigned: Bool, fromOffer: Bool) case paidMessagesRefunded(count: Int32, stars: Int64) case paidMessagesPriceEdited(stars: Int64, broadcastMessagesAllowed: Bool) @@ -396,7 +396,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } self = .prizeStars(amount: decoder.decodeInt64ForKey("amount", orElse: 0), isUnclaimed: decoder.decodeBoolForKey("unclaimed", orElse: false), boostPeerId: boostPeerId, transactionId: decoder.decodeOptionalStringForKey("transactionId"), giveawayMessageId: giveawayMessageId) case 44: - self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId"), upgradeSeparate: decoder.decodeOptionalBoolForKey("upgradeSeparate") ?? false, isAuctionAcquired: decoder.decodeOptionalBoolForKey("isAuctionAcquired") ?? false, toPeerId: decoder.decodeOptionalInt64ForKey("toPeerId").flatMap { EnginePeer.Id($0) }) + self = .starGift(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, convertStars: decoder.decodeOptionalInt64ForKey("convertStars"), text: decoder.decodeOptionalStringForKey("text"), entities: decoder.decodeOptionalObjectArrayWithDecoderForKey("entities"), nameHidden: decoder.decodeBoolForKey("nameHidden", orElse: false), savedToProfile: decoder.decodeBoolForKey("savedToProfile", orElse: false), converted: decoder.decodeBoolForKey("converted", orElse: false), upgraded: decoder.decodeBoolForKey("upgraded", orElse: false), canUpgrade: decoder.decodeBoolForKey("canUpgrade", orElse: false), upgradeStars: decoder.decodeOptionalInt64ForKey("upgradeStars"), isRefunded: decoder.decodeBoolForKey("isRefunded", orElse: false), isPrepaidUpgrade: decoder.decodeBoolForKey("isPrepaidUpgrade", orElse: false), upgradeMessageId: decoder.decodeOptionalInt32ForKey("upgradeMessageId"), peerId: decoder.decodeOptionalInt64ForKey("peerId").flatMap { EnginePeer.Id($0) }, senderId: decoder.decodeOptionalInt64ForKey("senderId").flatMap { EnginePeer.Id($0) }, savedId: decoder.decodeOptionalInt64ForKey("savedId"), prepaidUpgradeHash: decoder.decodeOptionalStringForKey("prepaidUpgradeHash"), giftMessageId: decoder.decodeOptionalInt32ForKey("giftMessageId"), upgradeSeparate: decoder.decodeOptionalBoolForKey("upgradeSeparate") ?? false, isAuctionAcquired: decoder.decodeOptionalBoolForKey("isAuctionAcquired") ?? false, toPeerId: decoder.decodeOptionalInt64ForKey("toPeerId").flatMap { EnginePeer.Id($0) }, number: decoder.decodeOptionalInt32ForKey("number")) case 45: var resaleAmount: CurrencyAmount? if let amount = decoder.decodeCodable(CurrencyAmount.self, forKey: "resaleAmount") { @@ -734,7 +734,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "giveawayMsgId") } - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, isAuctionAcquired, toPeerId): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, isAuctionAcquired, toPeerId, number): encoder.encodeInt32(44, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") if let convertStars { @@ -798,6 +798,11 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { encoder.encodeNil(forKey: "toPeerId") } + if let number { + encoder.encodeInt32(number, forKey: "number") + } else { + encoder.encodeNil(forKey: "number") + } case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, isRefunded, isPrepaidUpgrade, peerId, senderId, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, assigned, fromOffer): encoder.encodeInt32(45, forKey: "_rawValue") encoder.encodeObject(gift, forKey: "gift") @@ -945,7 +950,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { return [peerId] case let .prizeStars(_, _, boostPeerId, _, _): return boostPeerId.flatMap { [$0] } ?? [] - case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, toPeerId): + case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, peerId, senderId, _, _, _, _, _, toPeerId, _): var peerIds: [PeerId] = [] if let peerId { peerIds.append(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index dc7ce90d2a..a9108caa8f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -62,6 +62,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case lockedUntilDate case auctionSlug case auctionGiftsPerRound + case auctionStartDate + case upgradeVariantsCount } public struct Availability: Equatable, Codable, PostboxCoding { @@ -187,8 +189,10 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public let lockedUntilDate: Int32? public let auctionSlug: String? public let auctionGiftsPerRound: Int32? + public let auctionStartDate: Int32? + public let upgradeVariantsCount: Int32? - public init(id: Int64, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?, releasedBy: EnginePeer.Id?, perUserLimit: PerUserLimit?, lockedUntilDate: Int32?, auctionSlug: String?, auctionGiftsPerRound: Int32?) { + public init(id: Int64, title: String?, file: TelegramMediaFile, price: Int64, convertStars: Int64, availability: Availability?, soldOut: SoldOut?, flags: Flags, upgradeStars: Int64?, releasedBy: EnginePeer.Id?, perUserLimit: PerUserLimit?, lockedUntilDate: Int32?, auctionSlug: String?, auctionGiftsPerRound: Int32?, auctionStartDate: Int32?, upgradeVariantsCount: Int32?) { self.id = id self.title = title self.file = file @@ -203,6 +207,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.lockedUntilDate = lockedUntilDate self.auctionSlug = auctionSlug self.auctionGiftsPerRound = auctionGiftsPerRound + self.auctionStartDate = auctionStartDate + self.upgradeVariantsCount = upgradeVariantsCount } public init(from decoder: Decoder) throws { @@ -227,6 +233,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.lockedUntilDate = try container.decodeIfPresent(Int32.self, forKey: .lockedUntilDate) self.auctionSlug = try container.decodeIfPresent(String.self, forKey: .auctionSlug) self.auctionGiftsPerRound = try container.decodeIfPresent(Int32.self, forKey: .auctionGiftsPerRound) + self.auctionStartDate = try container.decodeIfPresent(Int32.self, forKey: .auctionStartDate) + self.upgradeVariantsCount = try container.decodeIfPresent(Int32.self, forKey: .upgradeVariantsCount) } public init(decoder: PostboxDecoder) { @@ -244,6 +252,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.lockedUntilDate = decoder.decodeOptionalInt32ForKey(CodingKeys.lockedUntilDate.rawValue) self.auctionSlug = decoder.decodeOptionalStringForKey(CodingKeys.auctionSlug.rawValue) self.auctionGiftsPerRound = decoder.decodeOptionalInt32ForKey(CodingKeys.auctionGiftsPerRound.rawValue) + self.auctionStartDate = decoder.decodeOptionalInt32ForKey(CodingKeys.auctionStartDate.rawValue) + self.upgradeVariantsCount = decoder.decodeOptionalInt32ForKey(CodingKeys.upgradeVariantsCount.rawValue) } public func encode(to encoder: Encoder) throws { @@ -267,6 +277,8 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encodeIfPresent(self.lockedUntilDate, forKey: .lockedUntilDate) try container.encodeIfPresent(self.auctionSlug, forKey: .auctionSlug) try container.encodeIfPresent(self.auctionGiftsPerRound, forKey: .auctionGiftsPerRound) + try container.encodeIfPresent(self.auctionStartDate, forKey: .auctionStartDate) + try container.encodeIfPresent(self.upgradeVariantsCount, forKey: .upgradeVariantsCount) } public func encode(_ encoder: PostboxEncoder) { @@ -320,6 +332,16 @@ public enum StarGift: Equatable, Codable, PostboxCoding { } else { encoder.encodeNil(forKey: CodingKeys.auctionGiftsPerRound.rawValue) } + if let auctionStartDate = self.auctionStartDate { + encoder.encodeInt32(auctionStartDate, forKey: CodingKeys.auctionStartDate.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.auctionStartDate.rawValue) + } + if let upgradeVariantsCount = self.upgradeVariantsCount { + encoder.encodeInt32(upgradeVariantsCount, forKey: CodingKeys.upgradeVariantsCount.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.upgradeVariantsCount.rawValue) + } } } @@ -342,6 +364,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { case releasedBy case valueAmount case valueCurrency + case valueUsdAmount case flags case themePeerId case peerColor @@ -632,13 +655,14 @@ public enum StarGift: Equatable, Codable, PostboxCoding { public let releasedBy: EnginePeer.Id? public let valueAmount: Int64? public let valueCurrency: String? + public let valueUsdAmount: Int64? public let flags: Flags public let themePeerId: EnginePeer.Id? public let peerColor: PeerCollectibleColor? public let hostPeerId: EnginePeer.Id? public let minOfferStars: Int64? - public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, flags: Flags, themePeerId: EnginePeer.Id?, peerColor: PeerCollectibleColor?, hostPeerId: EnginePeer.Id?, minOfferStars: Int64?) { + public init(id: Int64, giftId: Int64, title: String, number: Int32, slug: String, owner: Owner, attributes: [Attribute], availability: Availability, giftAddress: String?, resellAmounts: [CurrencyAmount]?, resellForTonOnly: Bool, releasedBy: EnginePeer.Id?, valueAmount: Int64?, valueCurrency: String?, valueUsdAmount: Int64?, flags: Flags, themePeerId: EnginePeer.Id?, peerColor: PeerCollectibleColor?, hostPeerId: EnginePeer.Id?, minOfferStars: Int64?) { self.id = id self.giftId = giftId self.title = title @@ -653,6 +677,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.releasedBy = releasedBy self.valueAmount = valueAmount self.valueCurrency = valueCurrency + self.valueUsdAmount = valueUsdAmount self.flags = flags self.themePeerId = themePeerId self.peerColor = peerColor @@ -690,6 +715,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.releasedBy = try container.decodeIfPresent(EnginePeer.Id.self, forKey: .releasedBy) self.valueAmount = try container.decodeIfPresent(Int64.self, forKey: .valueAmount) self.valueCurrency = try container.decodeIfPresent(String.self, forKey: .valueCurrency) + self.valueUsdAmount = try container.decodeIfPresent(Int64.self, forKey: .valueUsdAmount) self.flags = try container.decodeIfPresent(Int32.self, forKey: .flags).flatMap { Flags(rawValue: $0) } ?? [] self.themePeerId = try container.decodeIfPresent(Int64.self, forKey: .themePeerId).flatMap { EnginePeer.Id($0) } self.peerColor = try container.decodeIfPresent(PeerCollectibleColor.self, forKey: .peerColor) @@ -726,6 +752,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { self.releasedBy = decoder.decodeOptionalInt64ForKey(CodingKeys.releasedBy.rawValue).flatMap { EnginePeer.Id($0) } self.valueAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueAmount.rawValue) self.valueCurrency = decoder.decodeOptionalStringForKey(CodingKeys.valueCurrency.rawValue) + self.valueUsdAmount = decoder.decodeOptionalInt64ForKey(CodingKeys.valueUsdAmount.rawValue) self.flags = decoder.decodeOptionalInt32ForKey(CodingKeys.flags.rawValue).flatMap { Flags(rawValue: $0) } ?? [] self.themePeerId = decoder.decodeOptionalInt64ForKey(CodingKeys.themePeerId.rawValue).flatMap { EnginePeer.Id($0) } self.peerColor = decoder.decodeCodable(PeerCollectibleColor.self, forKey: CodingKeys.peerColor.rawValue) @@ -756,6 +783,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { try container.encodeIfPresent(self.releasedBy, forKey: .releasedBy) try container.encodeIfPresent(self.valueAmount, forKey: .valueAmount) try container.encodeIfPresent(self.valueCurrency, forKey: .valueCurrency) + try container.encodeIfPresent(self.valueUsdAmount, forKey: .valueUsdAmount) try container.encode(self.flags.rawValue, forKey: .flags) try container.encodeIfPresent(self.themePeerId?.toInt64(), forKey: .themePeerId) try container.encodeIfPresent(self.peerColor, forKey: .peerColor) @@ -802,6 +830,11 @@ public enum StarGift: Equatable, Codable, PostboxCoding { encoder.encodeNil(forKey: CodingKeys.valueAmount.rawValue) encoder.encodeNil(forKey: CodingKeys.valueCurrency.rawValue) } + if let valueUsdAmount = self.valueUsdAmount { + encoder.encodeInt64(valueUsdAmount, forKey: CodingKeys.valueUsdAmount.rawValue) + } else { + encoder.encodeNil(forKey: CodingKeys.valueUsdAmount.rawValue) + } encoder.encodeInt32(self.flags.rawValue, forKey: CodingKeys.flags.rawValue) if let themePeerId = self.themePeerId { encoder.encodeInt64(themePeerId.toInt64(), forKey: CodingKeys.themePeerId.rawValue) @@ -841,6 +874,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { releasedBy: self.releasedBy, valueAmount: self.valueAmount, valueCurrency: self.valueCurrency, + valueUsdAmount: self.valueUsdAmount, flags: self.flags, themePeerId: self.themePeerId, peerColor: self.peerColor, @@ -865,6 +899,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { releasedBy: self.releasedBy, valueAmount: self.valueAmount, valueCurrency: self.valueCurrency, + valueUsdAmount: self.valueUsdAmount, flags: self.flags, themePeerId: self.themePeerId, peerColor: self.peerColor, @@ -889,6 +924,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { releasedBy: self.releasedBy, valueAmount: self.valueAmount, valueCurrency: self.valueCurrency, + valueUsdAmount: self.valueUsdAmount, flags: self.flags, themePeerId: themePeerId, peerColor: self.peerColor, @@ -913,6 +949,7 @@ public enum StarGift: Equatable, Codable, PostboxCoding { releasedBy: self.releasedBy, valueAmount: self.valueAmount, valueCurrency: self.valueCurrency, + valueUsdAmount: self.valueUsdAmount, flags: self.flags, themePeerId: self.themePeerId, peerColor: self.peerColor, @@ -1006,7 +1043,7 @@ public extension StarGift { extension StarGift { init?(apiStarGift: Api.StarGift) { switch apiStarGift { - case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy, perUserTotal, perUserRemains, lockedUntilDate, auctionSlug, giftsPerRound): + case let .starGift(apiFlags, id, sticker, stars, availabilityRemains, availabilityTotal, availabilityResale, convertStars, firstSale, lastSale, upgradeStars, minResaleStars, title, releasedBy, perUserTotal, perUserRemains, lockedUntilDate, auctionSlug, giftsPerRound, auctionStartDate, upgradeVariantsCount): var flags = StarGift.Gift.Flags() if (apiFlags & (1 << 2)) != 0 { flags.insert(.isBirthdayGift) @@ -1041,8 +1078,25 @@ extension StarGift { guard let file = telegramMediaFileFromApiDocument(sticker, altDocuments: nil) else { return nil } - self = .generic(StarGift.Gift(id: id, title: title, file: file, price: stars, convertStars: convertStars, availability: availability, soldOut: soldOut, flags: flags, upgradeStars: upgradeStars, releasedBy: releasedBy?.peerId, perUserLimit: perUserLimit, lockedUntilDate: lockedUntilDate, auctionSlug: auctionSlug, auctionGiftsPerRound: giftsPerRound)) - case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, themePeer, peerColor, hostPeerId, minOfferStars): + self = .generic(StarGift.Gift( + id: id, + title: title, + file: file, + price: stars, + convertStars: convertStars, + availability: availability, + soldOut: soldOut, + flags: flags, + upgradeStars: upgradeStars, + releasedBy: releasedBy?.peerId, + perUserLimit: perUserLimit, + lockedUntilDate: lockedUntilDate, + auctionSlug: auctionSlug, + auctionGiftsPerRound: giftsPerRound, + auctionStartDate: auctionStartDate, + upgradeVariantsCount: upgradeVariantsCount + )) + case let .starGiftUnique(apiFlags, id, giftId, title, slug, num, ownerPeerId, ownerName, ownerAddress, attributes, availabilityIssued, availabilityTotal, giftAddress, resellAmounts, releasedBy, valueAmount, valueCurrency, valueUsdAmount, themePeer, peerColor, hostPeerId, minOfferStars): let owner: StarGift.UniqueGift.Owner if let ownerAddress { owner = .address(ownerAddress) @@ -1074,7 +1128,29 @@ extension StarGift { break } - self = .unique(StarGift.UniqueGift(id: id, giftId: giftId, title: title, number: num, slug: slug, owner: owner, attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) }, availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), giftAddress: giftAddress, resellAmounts: resellAmounts, resellForTonOnly: (apiFlags & (1 << 7)) != 0, releasedBy: releasedBy?.peerId, valueAmount: valueAmount, valueCurrency: valueCurrency, flags: flags, themePeerId: themePeer?.peerId, peerColor: peerCollectibleColor, hostPeerId: hostPeerId?.peerId, minOfferStars: minOfferStars.flatMap { Int64($0) })) + self = .unique(StarGift.UniqueGift( + id: id, + giftId: giftId, + title: title, + number: num, + slug: slug, + owner: owner, + attributes: attributes.compactMap { UniqueGift.Attribute(apiAttribute: $0) + }, + availability: UniqueGift.Availability(issued: availabilityIssued, total: availabilityTotal), + giftAddress: giftAddress, + resellAmounts: resellAmounts, + resellForTonOnly: (apiFlags & (1 << 7)) != 0, + releasedBy: releasedBy?.peerId, + valueAmount: valueAmount, + valueCurrency: valueCurrency, + valueUsdAmount: valueUsdAmount, + flags: flags, + themePeerId: themePeer?.peerId, + peerColor: peerCollectibleColor, + hostPeerId: hostPeerId?.peerId, + minOfferStars: minOfferStars.flatMap { Int64($0) } + )) } } } @@ -1439,7 +1515,8 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star collectionIds: nil, prepaidUpgradeHash: nil, upgradeSeparate: false, - dropOriginalDetailsStars: dropOriginalDetailsStars + dropOriginalDetailsStars: dropOriginalDetailsStars, + number: nil )) } } @@ -2393,6 +2470,7 @@ public final class ProfileGiftsContext { case prepaidUpgradeHash case upgradeSeparate case dropOriginalDetailsStars + case number } public let gift: TelegramCore.StarGift @@ -2415,6 +2493,7 @@ public final class ProfileGiftsContext { public let prepaidUpgradeHash: String? public let upgradeSeparate: Bool public let dropOriginalDetailsStars: Int64? + public let number: Int32? fileprivate let _fromPeerId: EnginePeer.Id? @@ -2442,7 +2521,8 @@ public final class ProfileGiftsContext { collectionIds: [Int32]?, prepaidUpgradeHash: String?, upgradeSeparate: Bool, - dropOriginalDetailsStars: Int64? + dropOriginalDetailsStars: Int64?, + number: Int32? ) { self.gift = gift self.reference = reference @@ -2465,6 +2545,7 @@ public final class ProfileGiftsContext { self.prepaidUpgradeHash = prepaidUpgradeHash self.upgradeSeparate = upgradeSeparate self.dropOriginalDetailsStars = dropOriginalDetailsStars + self.number = number } public init(from decoder: Decoder) throws { @@ -2497,6 +2578,7 @@ public final class ProfileGiftsContext { self.prepaidUpgradeHash = try container.decodeIfPresent(String.self, forKey: .prepaidUpgradeHash) self.upgradeSeparate = try container.decodeIfPresent(Bool.self, forKey: .upgradeSeparate) ?? false self.dropOriginalDetailsStars = try container.decodeIfPresent(Int64.self, forKey: .dropOriginalDetailsStars) + self.number = try container.decodeIfPresent(Int32.self, forKey: .number) } public func encode(to encoder: Encoder) throws { @@ -2522,6 +2604,7 @@ public final class ProfileGiftsContext { try container.encodeIfPresent(self.prepaidUpgradeHash, forKey: .prepaidUpgradeHash) try container.encode(self.upgradeSeparate, forKey: .upgradeSeparate) try container.encodeIfPresent(self.dropOriginalDetailsStars, forKey: .dropOriginalDetailsStars) + try container.encodeIfPresent(self.number, forKey: .number) } public func withGift(_ gift: TelegramCore.StarGift) -> StarGift { @@ -2545,7 +2628,8 @@ public final class ProfileGiftsContext { collectionIds: self.collectionIds, prepaidUpgradeHash: self.prepaidUpgradeHash, upgradeSeparate: self.upgradeSeparate, - dropOriginalDetailsStars: self.dropOriginalDetailsStars + dropOriginalDetailsStars: self.dropOriginalDetailsStars, + number: self.number ) } @@ -2570,7 +2654,8 @@ public final class ProfileGiftsContext { collectionIds: self.collectionIds, prepaidUpgradeHash: self.prepaidUpgradeHash, upgradeSeparate: self.upgradeSeparate, - dropOriginalDetailsStars: self.dropOriginalDetailsStars + dropOriginalDetailsStars: self.dropOriginalDetailsStars, + number: self.number ) } @@ -2595,7 +2680,8 @@ public final class ProfileGiftsContext { collectionIds: self.collectionIds, prepaidUpgradeHash: self.prepaidUpgradeHash, upgradeSeparate: self.upgradeSeparate, - dropOriginalDetailsStars: self.dropOriginalDetailsStars + dropOriginalDetailsStars: self.dropOriginalDetailsStars, + number: self.number ) } fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift { @@ -2619,7 +2705,8 @@ public final class ProfileGiftsContext { collectionIds: self.collectionIds, prepaidUpgradeHash: self.prepaidUpgradeHash, upgradeSeparate: self.upgradeSeparate, - dropOriginalDetailsStars: self.dropOriginalDetailsStars + dropOriginalDetailsStars: self.dropOriginalDetailsStars, + number: self.number ) } @@ -2644,7 +2731,8 @@ public final class ProfileGiftsContext { collectionIds: collectionIds, prepaidUpgradeHash: self.prepaidUpgradeHash, upgradeSeparate: self.upgradeSeparate, - dropOriginalDetailsStars: self.dropOriginalDetailsStars + dropOriginalDetailsStars: self.dropOriginalDetailsStars, + number: self.number ) } } @@ -2862,7 +2950,7 @@ public final class ProfileGiftsContext { extension ProfileGiftsContext.State.StarGift { init?(apiSavedStarGift: Api.SavedStarGift, peerId: EnginePeer.Id, transaction: Transaction) { switch apiSavedStarGift { - case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt, collectionIds, prepaidUpgradeHash, dropOriginalDetailsStars): + case let .savedStarGift(flags, fromId, date, apiGift, message, msgId, savedId, convertStars, upgradeStars, canExportDate, transferStars, canTransferAt, canResaleAt, collectionIds, prepaidUpgradeHash, dropOriginalDetailsStars, number): guard let gift = StarGift(apiStarGift: apiGift) else { return nil } @@ -2912,6 +3000,7 @@ extension ProfileGiftsContext.State.StarGift { self.prepaidUpgradeHash = prepaidUpgradeHash self.upgradeSeparate = (flags & (1 << 17)) != 0 self.dropOriginalDetailsStars = dropOriginalDetailsStars + self.number = number } } } @@ -3640,3 +3729,65 @@ public final class ResaleGiftsContext { return state } } + +func giftUpgradesId(giftId: Int64) -> ItemCacheEntryId { + let cacheKey = ValueBoxKey(length: 8) + cacheKey.setInt64(0, value: giftId) + return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedGiftUpgradesAttributes, key: cacheKey) +} + +final class CachedStartGiftUpgradeAttributes: Codable { + enum CodingKeys: String, CodingKey { + case attributes + } + + var attributes: [StarGift.UniqueGift.Attribute] + + init(attributes: [StarGift.UniqueGift.Attribute]) { + self.attributes = attributes + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.attributes = try container.decode([StarGift.UniqueGift.Attribute].self, forKey: .attributes) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(self.attributes, forKey: .attributes) + } +} + +func _internal_getStarGiftUpgradeAttributes(account: Account, giftId: Int64) -> Signal<[StarGift.UniqueGift.Attribute]?, NoError> { + return account.postbox.transaction { transaction in + if let cachedGifts = transaction.retrieveItemCacheEntry(id: giftUpgradesId(giftId: giftId))?.get(CachedStartGiftUpgradeAttributes.self) { + return .single(cachedGifts.attributes) + } else { + return account.network.request(Api.functions.payments.getStarGiftUpgradeAttributes(giftId: giftId)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { result -> Signal<[StarGift.UniqueGift.Attribute]?, NoError> in + guard let result else { + return .single(nil) + } + switch result { + case let .starGiftUpgradeAttributes(apiAttributes): + let attributes = apiAttributes.compactMap { StarGift.UniqueGift.Attribute(apiAttribute: $0) } + return account.postbox.transaction { transaction in + if !attributes.isEmpty { + if let entry = CodableEntry(CachedStartGiftUpgradeAttributes(attributes: attributes)) { + transaction.putItemCacheEntry(id: giftUpgradesId(giftId: giftId), entry: entry) + } + } + return attributes + } + } + } + } + } + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift index f213cc72b7..5fc8874823 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGiftsAuctions.swift @@ -30,8 +30,8 @@ private func _internal_getStarGiftAuctionState(postbox: Postbox, network: Networ } return postbox.transaction { transaction -> (gift: StarGift, state: GiftAuctionContext.State.AuctionState?, myState: GiftAuctionContext.State.MyState, timeout: Int32)? in switch result { - case let .starGiftAuctionState(apiGift, state, userState, timeout, users): - updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) + case let .starGiftAuctionState(apiGift, state, userState, timeout, users, chats): + updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(chats: chats, users: users)) guard let gift = StarGift(apiStarGift: apiGift) else { return nil } @@ -54,8 +54,27 @@ public final class GiftAuctionContext { public var date: Int32 } + public enum Round: Equatable { + case generic(num: Int32, duration: Int32) + case extendable(num: Int32, duration: Int32, extendTop: Int32, extendWindow: Int32) + + public var num: Int32 { + switch self { + case let .generic(num, _), let .extendable(num, _, _, _): + return num + } + } + + public var duration: Int32 { + switch self { + case let .generic(_, duration), let .extendable(_, duration, _, _): + return duration + } + } + } + public enum AuctionState: Equatable { - case ongoing(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [BidLevel], topBidders: [EnginePeer], nextRoundDate: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32) + case ongoing(version: Int32, startDate: Int32, endDate: Int32, minBidAmount: Int64, bidLevels: [BidLevel], topBidders: [EnginePeer], nextRoundDate: Int32, giftsLeft: Int32, currentRound: Int32, totalRounds: Int32, rounds: [Round], lastGiftNumber: Int32) case finished(startDate: Int32, endDate: Int32, averagePrice: Int64) } @@ -136,7 +155,7 @@ public final class GiftAuctionContext { private var currentVersion: Int32 { var currentVersion: Int32 = 0 - if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = self.auctionState { + if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = self.auctionState { currentVersion = version } return currentVersion @@ -154,7 +173,7 @@ public final class GiftAuctionContext { return } - if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion { + if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion { } else if let auctionState { self.auctionState = auctionState } @@ -163,7 +182,7 @@ public final class GiftAuctionContext { let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) var effectiveTimeout = timeout - if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _) = auctionState { + if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, _, _, _, _) = auctionState { let delta = nextRoundDate - currentTime if delta > 0 && delta < timeout { effectiveTimeout = delta @@ -184,7 +203,7 @@ public final class GiftAuctionContext { } func updateAuctionState(_ auctionState: GiftAuctionContext.State.AuctionState) { - if case let .ongoing(version, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion { + if case let .ongoing(version, _, _, _, _, _, _, _, _, _, _, _) = auctionState, version < self.currentVersion { } else { self.auctionState = auctionState } @@ -225,14 +244,23 @@ extension GiftAuctionContext.State.BidLevel { extension GiftAuctionContext.State.AuctionState { init?(apiAuctionState: Api.StarGiftAuctionState, peers: [PeerId: Peer]) { switch apiAuctionState { - case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, giftsLeft, currentRound, totalRounds): + case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, lastGiftNumber, giftsLeft, currentRound, totalRounds, apiRounds): var topBidders: [EnginePeer] = [] for peerId in topBiddersPeerIds { if let peer = peers[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(peerId))] { topBidders.append(EnginePeer(peer)) } } - self = .ongoing(version: version, startDate: startDate, endDate: endDate, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds) + var rounds: [GiftAuctionContext.State.Round] = [] + for apiRound in apiRounds { + switch apiRound { + case let .starGiftAuctionRound(num, duration): + rounds.append(.generic(num: num, duration: duration)) + case let .starGiftAuctionRoundExtendable(num, duration, extendTop, extendWindow): + rounds.append(.extendable(num: num, duration: duration, extendTop: extendTop, extendWindow: extendWindow)) + } + } + self = .ongoing(version: version, startDate: startDate, endDate: endDate, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds, rounds: rounds, lastGiftNumber: lastGiftNumber) case let .starGiftAuctionStateFinished(startDate, endDate, averagePrice): self = .finished(startDate: startDate, endDate: endDate, averagePrice: averagePrice) case .starGiftAuctionStateNotModified: @@ -242,14 +270,23 @@ extension GiftAuctionContext.State.AuctionState { init?(apiAuctionState: Api.StarGiftAuctionState, transaction: Transaction) { switch apiAuctionState { - case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, giftsLeft, currentRound, totalRounds): + case let .starGiftAuctionState(version, startDate, endDate, minBidAmount, bidLevels, topBiddersPeerIds, nextRoundAt, lastGiftNumber, giftsLeft, currentRound, totalRounds, apiRounds): var topBidders: [EnginePeer] = [] for peerId in topBiddersPeerIds { if let peer = transaction.getPeer(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(peerId))) { topBidders.append(EnginePeer(peer)) } } - self = .ongoing(version: version, startDate: startDate, endDate: endDate, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds) + var rounds: [GiftAuctionContext.State.Round] = [] + for apiRound in apiRounds { + switch apiRound { + case let .starGiftAuctionRound(num, duration): + rounds.append(.generic(num: num, duration: duration)) + case let .starGiftAuctionRoundExtendable(num, duration, extendTop, extendWindow): + rounds.append(.extendable(num: num, duration: duration, extendTop: extendTop, extendWindow: extendWindow)) + } + } + self = .ongoing(version: version, startDate: startDate, endDate: endDate, minBidAmount: minBidAmount, bidLevels: bidLevels.map(GiftAuctionContext.State.BidLevel.init(apiBidLevel:)), topBidders: topBidders, nextRoundDate: nextRoundAt, giftsLeft: giftsLeft, currentRound: currentRound, totalRounds: totalRounds, rounds: rounds, lastGiftNumber: lastGiftNumber) case let .starGiftAuctionStateFinished(startDate, endDate, averagePrice): self = .finished(startDate: startDate, endDate: endDate, averagePrice: averagePrice) case .starGiftAuctionStateNotModified: @@ -281,6 +318,7 @@ public struct GiftAuctionAcquiredGift: Equatable { public let position: Int32 public let text: String? public let entities: [MessageTextEntity]? + public let number: Int32? } func _internal_getGiftAuctionAcquiredGifts(account: Account, giftId: Int64) -> Signal<[GiftAuctionAcquiredGift], NoError> { @@ -302,7 +340,7 @@ func _internal_getGiftAuctionAcquiredGifts(account: Account, giftId: Int64) -> S var mappedGifts: [GiftAuctionAcquiredGift] = [] for gift in gifts { switch gift { - case let .starGiftAuctionAcquiredGift(flags, peerId, date, bidAmount, round, pos, message): + case let .starGiftAuctionAcquiredGift(flags, peerId, date, bidAmount, round, pos, message, number): if let peer = transaction.getPeer(peerId.peerId) { var text: String? var entities: [MessageTextEntity]? @@ -321,7 +359,8 @@ func _internal_getGiftAuctionAcquiredGifts(account: Account, giftId: Int64) -> S round: round, position: pos, text: text, - entities: entities + entities: entities, + number: number )) } } @@ -338,8 +377,8 @@ func _internal_getActiveGiftAuctions(account: Account, hash: Int64) -> Signal<[G |> mapToSignal { result in return account.postbox.transaction { transaction -> [GiftAuctionContext]? in switch result { - case let .starGiftActiveAuctions(auctions, users): - let parsedPeers = AccumulatedPeers(users: users) + case let .starGiftActiveAuctions(auctions, users, chats): + let parsedPeers = AccumulatedPeers(chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) var auctionContexts: [GiftAuctionContext] = [] @@ -493,7 +532,7 @@ public class GiftAuctionsManager { public extension GiftAuctionContext.State { func getPlace(myBid: Int64?, myBidDate: Int32?) -> Int32? { - guard case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _) = self.auctionState else { + guard case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _, _, _) = self.auctionState else { return nil } guard let myBid = myBid ?? self.myState.bidAmount else { @@ -550,7 +589,7 @@ public extension GiftAuctionContext.State { var startDate: Int32 { switch self.auctionState { - case let .ongoing(_, startDate, _, _, _, _, _, _, _, _): + case let .ongoing(_, startDate, _, _, _, _, _, _, _, _, _, _): return startDate case let .finished(startDate, _, _): return startDate @@ -559,7 +598,7 @@ public extension GiftAuctionContext.State { var endDate: Int32 { switch self.auctionState { - case let .ongoing(_, _, endDate, _, _, _, _, _, _, _): + case let .ongoing(_, _, endDate, _, _, _, _, _, _, _, _, _): return endDate case let .finished(_, endDate, _): return endDate diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 902808ae31..101466346d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -1671,7 +1671,8 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot collectionIds: nil, prepaidUpgradeHash: nil, upgradeSeparate: false, - dropOriginalDetailsStars: dropOriginalDetailsStars + dropOriginalDetailsStars: dropOriginalDetailsStars, + number: nil ) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index df269f3a72..ec9b54d7c2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -188,6 +188,10 @@ public extension TelegramEngine { public func sendStarGiftOffer(peerId: EnginePeer.Id, slug: String, amount: CurrencyAmount, duration: Int32, allowPaidStars: Int64?) -> Signal { return _internal_sendStarGiftOffer(account: self.account, peerId: peerId, slug: slug, amount: amount, duration: duration, allowPaidStars: allowPaidStars) } + + public func getStarGiftUpgradeAttributes(giftId: Int64) -> Signal<[StarGift.UniqueGift.Attribute]?, NoError> { + return _internal_getStarGiftUpgradeAttributes(account: self.account, giftId: giftId) + } } } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index ed139e7eb1..01c037b5c9 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1171,7 +1171,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = mutableString case .prizeStars: attributedString = NSAttributedString(string: strings.Notification_StarsPrize, font: titleFont, textColor: primaryTextColor) - case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate, isAuctionAcquired, _): + case let .starGift(gift, _, text, entities, _, _, _, _, _, upgradeStars, _, isPrepaidUpgrade, _, peerId, senderId, _, _, _, upgradeSeparate, isAuctionAcquired, _, _): if !forAdditionalServiceMessage { if let text { let mutableAttributedString = NSMutableAttributedString(attributedString: stringWithAppliedEntities(text, entities: entities ?? [], baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage())) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index df5f8f86aa..0d4deca999 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -280,7 +280,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { for media in item.message.media { if let action = media as? TelegramMediaAction { switch action.action { - case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): + case let .starGift(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy case let .starGiftUnique(gift, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): releasedBy = gift.releasedBy @@ -555,7 +555,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { buttonTitle = item.presentationData.strings.Notification_PremiumPrize_View hasServiceMessage = false } - case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _, _, toPeerId): + case let .starGift(gift, convertStars, giftText, giftEntities, _, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, isPrepaidUpgrade, _, channelPeerId, senderPeerId, _, _, _, _, _, toPeerId, _): var incoming = incoming var convertStars = convertStars if case let .generic(gift) = gift { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 266090f867..3f3ae06826 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -490,19 +490,29 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent actionTitle = item.presentationData.strings.Chat_ViewAlbum case "telegram_auction": var hasEnded = false + var isUpcoming = false + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) for attribute in webpage.attributes { if case let .giftAuction(giftAuction) = attribute { - let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + if case let .generic(gift) = giftAuction.gift, let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate { + isUpcoming = true + } if giftAuction.endDate < currentTime { hasEnded = true } break } } - subtitle = NSAttributedString(string: item.presentationData.strings.Chat_Auction, font: titleFont) text = nil - actionTitle = hasEnded ? item.presentationData.strings.Chat_Auction_ViewResults : item.presentationData.strings.Chat_Auction_Join - actionIcon = !hasEnded ? .bid : nil + if isUpcoming { + subtitle = NSAttributedString(string: item.presentationData.strings.Chat_Auction_Upcoming, font: titleFont) + actionTitle = item.presentationData.strings.Chat_Auction_View + actionIcon = nil + } else { + subtitle = NSAttributedString(string: item.presentationData.strings.Chat_Auction, font: titleFont) + actionTitle = hasEnded ? item.presentationData.strings.Chat_Auction_ViewResults : item.presentationData.strings.Chat_Auction_Join + actionIcon = !hasEnded ? .bid : nil + } default: break } diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index 01d0a629d1..9c2f87ad96 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -1327,7 +1327,7 @@ public final class EmojiStatusSelectionController: ViewController { return } if !self.context.isPremium || count < 3, let pushController = controller.pushController { - let controller = self.context.sharedContext.makeGiftWearPreviewScreen(context: self.context, gift: gift) + let controller = self.context.sharedContext.makeGiftWearPreviewScreen(context: self.context, gift: .unique(gift), attributes: nil) pushController(controller) } else { let _ = (self.context.engine.accountData.setStarGiftStatus(starGift: gift, expirationDate: nil) diff --git a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift index 4ceb2486c4..562c3f31ae 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftItemComponent/Sources/GiftItemComponent.swift @@ -539,7 +539,7 @@ public final class GiftItemComponent: Component { animationFile = gift.file backgroundColor = edgeColor secondBackgroundColor = centerColor - + emoji = ChatTextInputTextCustomEmojiAttribute( interactivelySelectedFromPackId: nil, fileId: gift.file.fileId.id, @@ -547,8 +547,14 @@ public final class GiftItemComponent: Component { ) let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) - let endTimeout = max(0, endTime - currentTime) + let endTimeout: Int32 + if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate { + endTimeout = max(0, auctionStartDate - currentTime) + animatedBadgeItems.append(AnimatedTextComponent.Item(id: "starts", content: .text(component.strings.Chat_Auction_StartsIn))) + } else { + endTimeout = max(0, endTime - currentTime) + } if endTimeout > 0 { let hours = Int(endTimeout / 3600) let minutes = Int((endTimeout % 3600) / 60) diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index ea2e795a02..23db34004f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -585,6 +585,7 @@ final class GiftOptionsScreenComponent: Component { self.starsItems[itemId] = visibleItem } + let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) var ribbon: GiftItemComponent.Ribbon? var outline: GiftItemComponent.Outline? var isSoldOut = false @@ -608,7 +609,11 @@ final class GiftOptionsScreenComponent: Component { let text: String var ribbonColor: GiftItemComponent.Ribbon.Color = .blue if gift.flags.contains(.isAuction) { - text = environment.strings.Gift_Options_Gift_Auction + if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate { + text = environment.strings.Gift_Options_Gift_Soon + } else { + text = environment.strings.Gift_Options_Gift_Auction + } ribbonColor = .orange outline = .orange } else if let perUserLimit = gift.perUserLimit, component.peerId.namespace != Namespaces.Peer.CloudChannel { @@ -624,7 +629,11 @@ final class GiftOptionsScreenComponent: Component { if !isSoldOut && gift.flags.contains(.requiresPremium) { let text: String if gift.flags.contains(.isAuction) { - text = environment.strings.Gift_Options_Gift_Auction + if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate { + text = environment.strings.Gift_Options_Gift_Soon + } else { + text = environment.strings.Gift_Options_Gift_Auction + } } else if component.context.isPremium, let perUserLimit = gift.perUserLimit { text = environment.strings.Gift_Options_Gift_Premium_Left(perUserLimit.remains) } else { @@ -661,7 +670,9 @@ final class GiftOptionsScreenComponent: Component { case let .generic(gift): if gift.flags.contains(.isAuction) { var action = environment.strings.Gift_Options_Gift_JoinAuction - if gift.availability?.remains == 0 { + if let auctionStartDate = gift.auctionStartDate, currentTime < auctionStartDate { + action = environment.strings.Gift_Options_Gift_ViewAuction + } else if gift.availability?.remains == 0 { action = environment.strings.Gift_Options_Gift_ViewAuction } subject = .starGift(gift: gift, price: action) diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD index cce609e7d9..7df5ae4068 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/BUILD @@ -46,11 +46,11 @@ swift_library( "//submodules/InAppPurchaseManager", "//submodules/Components/BlurredBackgroundComponent", "//submodules/ProgressNavigationButtonNode", - "//submodules/TelegramUI/Components/Gifts/GiftViewScreen", "//submodules/ConfettiEffect", "//submodules/TelegramUI/Components/EdgeEffect", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/MessageInputPanelComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift index 5ae83ff711..0175318100 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/ChatGiftPreviewItem.swift @@ -245,7 +245,7 @@ final class ChatGiftPreviewItemNode: ListViewItemNode { case let .starGift(gift): media = [ TelegramMediaAction( - action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false, isAuctionAcquired: false, toPeerId: nil) + action: .starGift(gift: .generic(gift), convertStars: gift.convertStars, text: item.text, entities: item.entities, nameHidden: false, savedToProfile: false, converted: false, upgraded: false, canUpgrade: gift.upgradeStars != nil, upgradeStars: item.upgradeStars, isRefunded: false, isPrepaidUpgrade: false, upgradeMessageId: nil, peerId: nil, senderId: nil, savedId: nil, prepaidUpgradeHash: nil, giftMessageId: nil, upgradeSeparate: false, isAuctionAcquired: false, toPeerId: nil, number: nil) ) ] } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index 5adbd507c2..080ca37fc3 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -32,7 +32,6 @@ import InAppPurchaseManager import BlurredBackgroundComponent import ProgressNavigationButtonNode import Markdown -import GiftViewScreen import UndoUI import ConfettiEffect import EdgeEffect @@ -1596,10 +1595,7 @@ private final class GiftSetupScreenComponent: Component { guard let self, let component = self.component, let controller = self.environment?.controller(), let upgradePreview else { return } - let previewController = GiftViewScreen( - context: component.context, - subject: .upgradePreview(upgradePreview.attributes, peerName) - ) + let previewController = component.context.sharedContext.makeGiftUpgradePreviewScreen(context: component.context, attributes: upgradePreview.attributes, peerName: peerName) controller.push(previewController) }) } @@ -1712,7 +1708,7 @@ private final class GiftSetupScreenComponent: Component { var remains: Int32 = availability.remains if let auctionState = self.giftAuctionState { switch auctionState.auctionState { - case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _): + case let .ongoing(_, _, _, _, _, _, _, giftsLeft, _, _, _, _): remains = giftsLeft case .finished: remains = 0 @@ -1839,7 +1835,7 @@ private final class GiftSetupScreenComponent: Component { ))) if let giftAuctionState = self.giftAuctionState { switch giftAuctionState.auctionState { - case let .ongoing(_, _, endTime, _, _, _, _, _, _, _): + case let .ongoing(_, _, endTime, _, _, _, _, _, _, _, _, _): let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) let endTimeout = max(0, endTime - currentTime) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift index 49e8bd7b16..574e2a4d45 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionAcquiredScreen.swift @@ -328,11 +328,21 @@ private final class GiftAuctionAcquiredScreenComponent: Component { var giftSubject: GiftItemComponent.Subject? + var giftTitle: String = "" if case let .generic(gift) = component.gift { giftSubject = .starGift(gift: gift, price: "") + giftTitle = gift.title ?? "" } if let giftSubject { + let titleString: String + if let number = gift.number { + let fullGiftTitle = "\(giftTitle) #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))" + titleString = environment.strings.Gift_Acquired_GiftRound(fullGiftTitle, "\(gift.round)").string + } else { + titleString = environment.strings.Gift_Acquired_Round("\(gift.round)").string + } + items.append(.init( id: "header", title: nil, @@ -351,7 +361,7 @@ private final class GiftAuctionAcquiredScreenComponent: Component { AnyComponentWithIdentity( id: "title", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_Acquired_Round("\(gift.round)").string, font: tableBoldFont, textColor: tableTextColor))) + MultilineTextComponent(text: .plain(NSAttributedString(string: titleString, font: tableBoldFont, textColor: tableTextColor))) ) ) ], spacing: 1.0)) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift index 69b0b52f8e..0a22e3e2b1 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionActiveBidsScreen.swift @@ -663,7 +663,7 @@ private final class ActiveAuctionComponent: Component { var titleText: String = "" var subtitleText: String = "" var subtitleTextColor = component.theme.list.itemPrimaryTextColor - if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, currentRound, totalRound) = component.state.auctionState, let myBid = component.state.myState.bidAmount { + if case let .ongoing(_, _, _, _, _, _, nextRoundDate, _, currentRound, totalRound, _, _) = component.state.auctionState, let myBid = component.state.myState.bidAmount { titleText = component.strings.Gift_ActiveAuctions_Round("\(currentRound)", "\(totalRound)").string let bidString = "#\(presentationStringsFormattedNumber(Int32(clamping: myBid), component.dateTimeFormat.groupingSeparator))" diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift index 07629d6fb4..75ff3ecc07 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionBidScreen.swift @@ -216,6 +216,12 @@ private final class BadgeComponent: Component { subtitleView.alpha = 1.0 } badgeOffset.y -= 6.0 + UIScreenPixel + + let subtitleBadgeWidth = subtitleSize.width + 60.0 + if subtitleBadgeWidth > badgeWidth { + badgeOffset.x -= (subtitleBadgeWidth - badgeWidth) * 0.5 + badgeWidth = subtitleBadgeWidth + } } else if let subtitleView = self.subtitle.view { subtitleView.alpha = 0.0 } @@ -450,7 +456,7 @@ private final class PeerPlaceComponent: Component { if let place = component.place { placeString = presentationStringsFormattedNumber(place, component.groupingSeparator) if place >= 100 { - placeString = "\(placeString)+" + placeString = "\(compactNumericCountString(Int(place), decimalSeparator: ".", showDecimalPart: false))+" } } else { placeString = "–" @@ -1817,7 +1823,7 @@ private final class GiftAuctionBidScreenComponent: Component { } var minBidAmount: Int64 = 100 - if case let .ongoing(_, _, _, auctionMinBidAmount, _, _, _, _, _, _) = auctionState.auctionState { + if case let .ongoing(_, _, _, auctionMinBidAmount, _, _, _, _, _, _, _, _) = auctionState.auctionState { minBidAmount = auctionMinBidAmount if let myMinBidAmount = auctionState.myState.minBidAmount { minBidAmount = myMinBidAmount @@ -1858,7 +1864,7 @@ private final class GiftAuctionBidScreenComponent: Component { } var minBidAmount: Int64 = 100 var maxBidAmount: Int64 = 50000 - if case let .ongoing(_, _, _, auctionMinBidAmount, bidLevels, _, _, _, _, _) = state.auctionState { + if case let .ongoing(_, _, _, auctionMinBidAmount, bidLevels, _, _, _, _, _, _, _) = state.auctionState { minBidAmount = auctionMinBidAmount if let firstLevel = bidLevels.first(where: { $0.position == 1 }) { maxBidAmount = max(maxBidAmount, Int64(Double(firstLevel.amount) * 1.5)) @@ -2192,7 +2198,7 @@ private final class GiftAuctionBidScreenComponent: Component { var topBidsComponents: [(EnginePeer.Id, AnyComponent)] = [] let place: Int32 - if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, topBidders, _, _, _, _) = giftAuctionState.auctionState { + if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, topBidders, _, _, _, _, _, lastGiftNumber) = giftAuctionState.auctionState { var myBidAmount = Int64(self.amount.realValue) var myBidDate = currentTime var isBiddingUp = true @@ -2208,6 +2214,9 @@ private final class GiftAuctionBidScreenComponent: Component { var bidTitle: String var bidTitleColor: UIColor var bidStatus: PeerComponent.Status? + + var giftTitle: String? + var giftNumber: Int32? if isBiddingUp { bidTitleColor = environment.theme.list.itemSecondaryTextColor bidTitle = environment.strings.Gift_AuctionBid_BidPreview @@ -2221,12 +2230,16 @@ private final class GiftAuctionBidScreenComponent: Component { bidStatus = .outbid } else { bidTitle = environment.strings.Gift_AuctionBid_Winning - bidTitleColor = environment.theme.list.itemDisclosureActions.constructive.fillColor + bidTitleColor = UIColor(rgb: 0x53a939) bidStatus = .winning + if case let .generic(gift) = giftAuctionState.gift { + giftTitle = gift.title + giftNumber = lastGiftNumber + place + } } if let peer = self.peersMap[component.context.account.peerId] { - myBidTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: bidTitle.uppercased(), font: Font.medium(13.0), textColor: bidTitleColor)))) + myBidTitleComponent = AnyComponent(PeerHeaderComponent(color: bidTitleColor, dateTimeFormat: environment.dateTimeFormat, title: bidTitle, giftTitle: giftTitle, giftNumber: giftNumber)) myBidComponent = AnyComponent(PeerComponent(context: component.context, theme: environment.theme, groupingSeparator: environment.dateTimeFormat.groupingSeparator, peer: peer, place: place, amount: myBidAmount, status: bidStatus, isLast: true, action: nil)) } @@ -2244,7 +2257,7 @@ private final class GiftAuctionBidScreenComponent: Component { } if !topBidsComponents.isEmpty { - topBidsTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_TopWinners.uppercased(), font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor)))) + topBidsTitleComponent = AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_TopWinnersTotal("\(giftsPerRound)").string.uppercased(), font: Font.medium(13.0), textColor: environment.theme.list.itemSecondaryTextColor)))) } } else { place = 1 @@ -2252,7 +2265,7 @@ private final class GiftAuctionBidScreenComponent: Component { var topCutoffRealValue: Int? if place > giftsPerRound { - if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _) = giftAuctionState.auctionState { + if let giftAuctionState = self.giftAuctionState, case let .ongoing(_, _, _, _, bidLevels, _, _, _, _, _, _, _) = giftAuctionState.auctionState { for bidLevel in bidLevels { if bidLevel.position == giftsPerRound - 1 { topCutoffRealValue = Int(bidLevel.amount) @@ -2368,93 +2381,112 @@ private final class GiftAuctionBidScreenComponent: Component { var auctionStats: [([AnimatedTextComponent.Item], String)] = [] var minBidAnimatedItems: [AnimatedTextComponent.Item] = [] - var untilNextDropAnimatedItems: [AnimatedTextComponent.Item] = [] + var untilNextRoundAnimatedItems: [AnimatedTextComponent.Item] = [] var dropsLeftAnimatedItems: [AnimatedTextComponent.Item] = [] - if let auctionState = self.giftAuctionState?.auctionState { - if case let .ongoing(_, _, _, minBidAmount, _, _, nextDropDate, dropsLeft, _, _) = auctionState { - var minBidAmount = minBidAmount - if let myMinBidAmmount = self.giftAuctionState?.myState.minBidAmount { - minBidAmount = myMinBidAmmount - } - var minBidString: String - if minBidAmount > 99999 { - minBidString = compactNumericCountString(Int(minBidAmount), decimalSeparator: environment.dateTimeFormat.decimalSeparator, showDecimalPart: false) - } else { - minBidString = presentationStringsFormattedNumber(Int32(clamping: minBidAmount), environment.dateTimeFormat.groupingSeparator) - } - minBidString = "# \(minBidString)" - if let hashIndex = minBidString.firstIndex(of: "#") { - var prefix = String(minBidString[.. 99999 { + minBidString = compactNumericCountString(Int(minBidAmount), decimalSeparator: environment.dateTimeFormat.decimalSeparator, showDecimalPart: false) + } else { + minBidString = presentationStringsFormattedNumber(Int32(clamping: minBidAmount), environment.dateTimeFormat.groupingSeparator) + } + minBidString = "# \(minBidString)" + if let hashIndex = minBidString.firstIndex(of: "#") { + var prefix = String(minBidString[.. 0 { + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "h", content: .number(hours, minDigits: 1))) + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "colon1", content: .text(":"))) + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "colon2", content: .text(":"))) + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } else { + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "m", content: .number(minutes, minDigits: 2))) + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "colon", content: .text(":"))) + untilNextRoundAnimatedItems.append(AnimatedTextComponent.Item(id: "s", content: .number(seconds, minDigits: 2))) + } + + dropsLeftAnimatedItems = [AnimatedTextComponent.Item(id: "drops", content: .number(Int(dropsLeft), minDigits: 1))] } auctionStats.append(( @@ -2463,8 +2495,8 @@ private final class GiftAuctionBidScreenComponent: Component { )) auctionStats.append(( - untilNextDropAnimatedItems, - environment.strings.Gift_AuctionBid_UntilNext + untilNextRoundAnimatedItems, + nextRoundTitle )) auctionStats.append(( @@ -2665,10 +2697,18 @@ private final class GiftAuctionBidScreenComponent: Component { containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) + var subtitleString = environment.strings.Gift_AuctionBid_Subtitle("\(giftsPerRound)").string + if let auctionState = self.giftAuctionState?.auctionState, case let .ongoing(_, _, _, _, _, _, _, _, currentRound, totalRounds, _, _) = auctionState { + subtitleString = environment.strings.Gift_AuctionBid_RoundSubtitle( + presentationStringsFormattedNumber(currentRound, environment.dateTimeFormat.groupingSeparator), + presentationStringsFormattedNumber(totalRounds, environment.dateTimeFormat.groupingSeparator), + ).string + } + let subtitleSize = self.subtitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.Gift_AuctionBid_Subtitle("\(giftsPerRound)").string, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)) + text: .plain(NSAttributedString(string: subtitleString, font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor)) )), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) @@ -3474,3 +3514,101 @@ private final class GiftViewContextReferenceContentSource: ContextReferenceConte return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +private final class PeerHeaderComponent: CombinedComponent { + let color: UIColor + let dateTimeFormat: PresentationDateTimeFormat + let title: String + let giftTitle: String? + let giftNumber: Int32? + + public init( + color: UIColor, + dateTimeFormat: PresentationDateTimeFormat, + title: String, + giftTitle: String?, + giftNumber: Int32? + ) { + self.color = color + self.dateTimeFormat = dateTimeFormat + self.title = title + self.giftTitle = giftTitle + self.giftNumber = giftNumber + } + + static func ==(lhs: PeerHeaderComponent, rhs: PeerHeaderComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.giftTitle != rhs.giftTitle { + return false + } + if lhs.giftNumber != rhs.giftNumber { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + + let background = Child(RoundedRectangle.self) + let giftTitle = Child(MultilineTextComponent.self) + + return { context in + let component = context.component + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title.uppercased(), font: Font.medium(13.0), textColor: component.color)) + ), + availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height), + transition: .immediate + ) + context.add(title + .position(CGPoint(x: title.size.width / 2.0, y: title.size.height / 2.0)) + ) + + var contentSize = title.size + if let titleString = component.giftTitle, let giftNumber = component.giftNumber { + let giftTitle = giftTitle.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: "\(titleString) #\(formatCollectibleNumber(giftNumber, dateTimeFormat: component.dateTimeFormat))", font: Font.regular(12.0), textColor: component.color)) + ), + availableSize: CGSize(width: context.availableSize.width - 16.0, height: context.availableSize.height), + transition: .immediate + ) + + let spacing: CGFloat = 6.0 + let padding: CGFloat = 5.0 + let backgroundSize = CGSize(width: giftTitle.size.width + padding * 2.0, height: giftTitle.size.height + 4.0) + let background = background.update( + component: RoundedRectangle( + color: component.color.withAlphaComponent(0.1), + cornerRadius: backgroundSize.height / 2.0 + ), + availableSize: backgroundSize, + transition: .immediate + ) + context.add(background + .position(CGPoint(x: title.size.width + spacing + padding + giftTitle.size.width / 2.0, y: title.size.height / 2.0)) + .appear(.default()) + .disappear(.default()) + ) + + context.add(giftTitle + .position(CGPoint(x: title.size.width + spacing + padding + giftTitle.size.width / 2.0, y: title.size.height / 2.0)) + .appear(.default()) + .disappear(.default()) + ) + + contentSize.width += spacing + backgroundSize.width + } + + return contentSize + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift index da94c92f4e..e4086accc0 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionInfoScreen.swift @@ -80,7 +80,7 @@ private final class GiftAuctionInfoSheetContent: CombinedComponent { let _ = (self.auctionContext.state |> deliverOnMainQueue).startStandalone(next: { [weak self] state in - if let self, case let .ongoing(_, _, _, _, _, _, _, _, _, totalRounds) = state?.auctionState { + if let self, case let .ongoing(_, _, _, _, _, _, _, _, _, totalRounds, _, _) = state?.auctionState { self.rounds = totalRounds self.updated() } diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift new file mode 100644 index 0000000000..b963cfc73c --- /dev/null +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionUpgradePreviewScreen.swift @@ -0,0 +1,427 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import MultilineTextWithEntitiesComponent +import BundleIconComponent +import ButtonComponent +import Markdown +import BalancedTextComponent +import AvatarNode +import TextFormat +import TelegramStringFormatting +import StarsAvatarComponent +import EmojiTextAttachmentView +import EmojiStatusComponent +import UndoUI +import ConfettiEffect +import PlainButtonComponent +import CheckComponent +import TooltipUI +import LottieComponent +import ContextUI +import TelegramNotices +import PremiumLockButtonSubtitleComponent +import StarsBalanceOverlayComponent +import BalanceNeededScreen +import GiftItemComponent +import GiftAnimationComponent +import ChatThemeScreen +import ProfileLevelRatingBarComponent +import AnimatedTextComponent + +private final class GiftAuctionUpgradePreviewSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let gift: StarGift + let animateOut: ActionSlot> + let getController: () -> ViewController? + + init( + context: AccountContext, + gift: StarGift, + animateOut: ActionSlot>, + getController: @escaping () -> ViewController? + ) { + self.context = context + self.gift = gift + self.animateOut = animateOut + self.getController = getController + } + + static func ==(lhs: GiftAuctionUpgradePreviewSheetContent, rhs: GiftAuctionUpgradePreviewSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gift != rhs.gift { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + private var peerDisposable: Disposable? + var peerMap: [EnginePeer.Id: EnginePeer] = [:] + + var cachedChevronImage: (UIImage, PresentationTheme)? + var cachedSmallChevronImage: (UIImage, PresentationTheme)? + + var upgradePreviewTimer: SwiftSignalKit.Timer? + + init( + context: AccountContext + ) { + self.context = context + + super.init() + + let peerIds: [EnginePeer.Id] = [context.account.peerId] + + self.peerDisposable = ( + context.engine.data.get(EngineDataMap( + peerIds.map { peerId -> TelegramEngine.EngineData.Item.Peer.Peer in + return TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + } + )) + |> deliverOnMainQueue + ).startStrict(next: { [weak self] peers in + if let strongSelf = self { + var peersMap: [EnginePeer.Id: EnginePeer] = [:] + for (peerId, maybePeer) in peers { + if let peer = maybePeer { + peersMap[peerId] = peer + } + } + strongSelf.peerMap = peersMap + strongSelf.updated(transition: .immediate) + } + }) + + self.upgradePreviewTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: true, completion: { [weak self] in + self?.upgradePreviewTimerTick() + }, queue: Queue.mainQueue()) + self.upgradePreviewTimer?.start() + self.upgradePreviewTimerTick() + } + + deinit { + self.peerDisposable?.dispose() + self.upgradePreviewTimer?.invalidate() + } + + private func upgradePreviewTimerTick() { + + } + } + + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + let animation = Child(GiftCompositionComponent.self) + let avatar = Child(AvatarComponent.self) + let peerName = Child(MultilineTextComponent.self) + //let description = Child(MultilineTextComponent.self) + + //let initialGift = Child(GiftItemComponent.self) + //let upgradedGift = Child(GiftItemComponent.self) + + let button = Child(ButtonComponent.self) + + let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let nameDisplayOrder = component.context.sharedContext.currentPresentationData.with { $0 }.nameDisplayOrder + let controller = environment.controller + let state = context.state + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + + var contentHeight: CGFloat = 0.0 + + let headerHeight: CGFloat = 226.0 + let headerSubject: GiftCompositionComponent.Subject = .preview([]) + + var peerNameString = "" + if let peer = state.peerMap[component.context.account.peerId] { + peerNameString = peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) + } + + let peerName = peerName.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: peerNameString, + font: Font.bold(20.0), + textColor: .white, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let animationOffset: CGPoint? = CGPoint(x: peerName.size.width / 2.0 + 20.0 - 12.0, y: 79.0) + let animationScale: CGFloat = 0.19 + + let animation = animation.update( + component: GiftCompositionComponent( + context: component.context, + theme: environment.theme, + subject: headerSubject, + animationOffset: animationOffset, + animationScale: animationScale, + displayAnimationStars: true, + externalState: giftCompositionExternalState, + requestUpdate: { [weak state] transition in + state?.updated(transition: transition) + } + ), + availableSize: CGSize(width: context.availableSize.width, height: headerHeight), + transition: context.transition + ) + context.add(animation + .position(CGPoint(x: context.availableSize.width / 2.0, y: headerHeight / 2.0)) + ) + contentHeight += headerHeight + + if let peer = state.peerMap[component.context.account.peerId] { + let avatar = avatar.update( + component: AvatarComponent( + context: component.context, + theme: theme, + peer: peer + ), + environment: {}, + availableSize: CGSize(width: 100.0, height: 100.0), + transition: context.transition + ) + context.add(avatar + .position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0)) + ) + } + context.add(peerName + .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0)) + ) + +// let vibrantColor: UIColor +// if let previewPatternColor = giftCompositionExternalState.previewPatternColor { +// vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) +// } else { +// vibrantColor = UIColor.white.withAlphaComponent(0.6) +// } + + + let buttonInsets = ContainerViewLayout.concentricInsets(bottomInset: environment.safeInsets.bottom, innerDiameter: 52.0, sideInset: 30.0) + let buttonSize = CGSize(width: context.availableSize.width - buttonInsets.left - buttonInsets.right, height: 52.0) + let buttonBackground = ButtonComponent.Background( + style: .glass, + color: theme.list.itemCheckColors.fillColor, + foreground: theme.list.itemCheckColors.foregroundColor, + pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) + ) + let button = button.update( + component: ButtonComponent( + background: buttonBackground, + content: AnyComponentWithIdentity( + id: AnyHashable("ok"), + component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Common_OK, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + ), + isEnabled: true, + displaysProgress: false, + action: { + if let controller = controller() as? GiftAuctionUpgradePreviewScreen { + controller.dismissAnimated() + } + }), + availableSize: buttonSize, + transition: context.transition + ) + + let buttonFrame = CGRect(origin: CGPoint(x: buttonInsets.left, y: contentHeight), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + contentHeight += button.size.height + contentHeight += 7.0 + + let effectiveBottomInset: CGFloat = environment.metrics.isTablet ? 0.0 : environment.safeInsets.bottom + return CGSize(width: context.availableSize.width, height: contentHeight + 5.0 + effectiveBottomInset) + } + } +} + +final class GiftAuctionUpgradePreviewSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let gift: StarGift + + init( + context: AccountContext, + gift: StarGift + ) { + self.context = context + self.gift = gift + } + + static func ==(lhs: GiftAuctionUpgradePreviewSheetComponent, rhs: GiftAuctionUpgradePreviewSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.gift != rhs.gift { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + let sheetExternalState = SheetComponent.ExternalState() + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(GiftAuctionUpgradePreviewSheetContent( + context: context.component.context, + gift: context.component.gift, + animateOut: animateOut, + getController: controller + )), + style: .glass, + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + hasDimView: false, + autoAnimateOut: false, + externalState: sheetExternalState, + animateOut: animateOut, + onPan: { + }, + willDismiss: { + if let controller = controller() as? GiftAuctionUpgradePreviewScreen { + controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition())) + } + } + ), + environment: { + environment + SheetComponentEnvironment( + isDisplaying: environment.value.isVisible, + isCentered: environment.metrics.widthClass == .regular, + hasInputHeight: !environment.inputHeight.isZero, + regularMetricsSize: CGSize(width: 430.0, height: 900.0), + dismiss: { animated in + if animated { + if let controller = controller() as? GiftAuctionUpgradePreviewScreen { + controller.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.3).withUserData(ViewControllerComponentContainer.AnimateOutTransition())) + animateOut.invoke(Action { _ in + controller.dismiss(completion: nil) + }) + } + } else { + if let controller = controller() as? GiftAuctionUpgradePreviewScreen { + controller.dismiss(completion: nil) + } + } + } + ) + }, + availableSize: context.availableSize, + transition: context.transition + ) + + context.add(sheet + .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) + ) + + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + var sideInset: CGFloat = 0.0 + var bottomInset: CGFloat = max(environment.safeInsets.bottom, sheetExternalState.contentHeight) + if case .regular = environment.metrics.widthClass { + sideInset = floor((context.availableSize.width - 430.0) / 2.0) - 12.0 + bottomInset = (context.availableSize.height - sheetExternalState.contentHeight) / 2.0 + sheetExternalState.contentHeight + } + + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: bottomInset, right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: max(sideInset, environment.safeInsets.left), bottom: 0.0, right: max(sideInset, environment.safeInsets.right)), + additionalInsets: .zero, + statusBarHeight: environment.statusBarHeight, + inputHeight: nil, + inputHeightIsInteractivellyChanging: false, + inVoiceOver: false + ) + controller.presentationContext.containerLayoutUpdated(layout, transition: context.transition.containedViewLayoutTransition) + } + + return context.availableSize + } + } +} + +public class GiftAuctionUpgradePreviewScreen: ViewControllerComponentContainer { + private let context: AccountContext + + public init( + context: AccountContext, + gift: StarGift + ) { + self.context = context + + super.init( + context: context, + component: GiftAuctionUpgradePreviewSheetComponent(context: context, gift: gift), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: .default + ) + + self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } +} diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 9348390dbc..7688e1b171 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -482,17 +482,23 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let tableTextColor = theme.list.itemPrimaryTextColor let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970) + var startTime = currentTime var endTime = currentTime + var isUpcoming = false var isEnded = false var tableItems: [TableComponent.Item] = [] if let auctionState = state.giftAuctionState, case let .generic(gift) = component.auctionContext.gift { + startTime = auctionState.startDate endTime = auctionState.endDate if case .finished = auctionState.auctionState { isEnded = true } else if auctionState.endDate < currentTime { isEnded = true } + if auctionState.startDate > currentTime { + isUpcoming = true + } if isEnded { descriptionText = strings.Gift_Auction_Ended @@ -574,33 +580,80 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { tableItems.append(.init( id: "start", - title: strings.Gift_Auction_Started, + title: isUpcoming ? strings.Gift_Auction_Start : strings.Gift_Auction_Started, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.startDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) tableItems.append(.init( id: "ends", - title: strings.Gift_Auction_Ends, + title: isUpcoming ? strings.Gift_Auction_End : strings.Gift_Auction_Ends, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: auctionState.endDate, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) )) - if case let .ongoing(_, _, _, _, _, _, _, giftsLeft, currentRound, totalRounds) = auctionState.auctionState { - tableItems.append(.init( - id: "round", - title: strings.Gift_Auction_CurrentRound, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_Round("\(currentRound)", "\(totalRounds)").string, font: tableFont, textColor: tableTextColor))) - ) - )) - tableItems.append(.init( - id: "availability", - title: strings.Gift_Auction_Availability, - component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_AvailabilityOf(presentationStringsFormattedNumber(giftsLeft, dateTimeFormat.groupingSeparator), presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator)).string, font: tableFont, textColor: tableTextColor))) - ) - )) + + if case let .ongoing(_, _, _, _, _, _, _, giftsLeft, currentRound, totalRounds, rounds, _) = auctionState.auctionState { + if isUpcoming { + tableItems.append(.init( + id: "quantity", + title: strings.Gift_Auction_Quantity, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator), font: tableFont, textColor: tableTextColor))) + ) + )) + tableItems.append(.init( + id: "rounds", + title: strings.Gift_Auction_TotalRounds, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: presentationStringsFormattedNumber(totalRounds, dateTimeFormat.groupingSeparator), font: tableFont, textColor: tableTextColor))) + ) + )) + + for i in 0 ..< rounds.count { + let round = rounds[i] + let start = round.num + var end = totalRounds + if i < rounds.count - 1 { + let nextRound = rounds[i + 1] + end = nextRound.num - 1 + } + + let title: String = start == end ? strings.Gift_Auction_TimeRound("\(start)").string : strings.Gift_Auction_TimeRounds("\(start)-\(end)").string + let value: String + if round.duration % 3600 == 0 { + let hours = round.duration / 3600 + value = start == end ? strings.Gift_Auction_Hours(hours) : strings.Gift_Auction_HoursEach(hours) + } else { + let minutes = round.duration / 60 + value = start == end ? strings.Gift_Auction_Minutes(minutes) : strings.Gift_Auction_MinutesEach(minutes) + } + + tableItems.append(.init( + id: "round_\(i)", + title: title, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: value, font: tableFont, textColor: tableTextColor))) + ) + )) + } + + } else { + tableItems.append(.init( + id: "round", + title: strings.Gift_Auction_CurrentRound, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_Round("\(currentRound)", "\(totalRounds)").string, font: tableFont, textColor: tableTextColor))) + ) + )) + tableItems.append(.init( + id: "availability", + title: strings.Gift_Auction_Availability, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Gift_Auction_AvailabilityOf(presentationStringsFormattedNumber(giftsLeft, dateTimeFormat.groupingSeparator), presentationStringsFormattedNumber(gift.availability?.total ?? 0, dateTimeFormat.groupingSeparator)).string, font: tableFont, textColor: tableTextColor))) + ) + )) + } } } } @@ -713,13 +766,8 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { guard let state else { return } - #if DEBUG - let giftController = GiftUpgradePreviewScreen(context: component.context) - environment.controller()?.push(giftController) - #else let giftController = GiftAuctionAcquiredScreen(context: component.context, gift: component.auctionContext.gift, acquiredGifts: state.giftAuctionAcquiredGifts) environment.controller()?.push(giftController) - #endif }, animateScale: false), availableSize: CGSize(width: context.availableSize.width - 64.0, height: context.availableSize.height), transition: context.transition @@ -839,21 +887,32 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { let buttonChild: _UpdatedChildComponent if !isEnded { - let buttonAttributedString = NSMutableAttributedString(string: strings.Gift_Auction_Join, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) + let buttonAttributedString = NSMutableAttributedString(string: isUpcoming ? strings.Gift_Auction_EarlyBid : strings.Gift_Auction_Join, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center) - let endTimeout = max(0, endTime - currentTime) + let endTimeout: Int32 + if currentTime < startTime { + endTimeout = max(0, startTime - currentTime) + } else { + endTimeout = max(0, endTime - currentTime) + } let hours = Int(endTimeout / 3600) let minutes = Int((endTimeout % 3600) / 60) let seconds = Int(endTimeout % 60) - let rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes + let rawString: String + if isUpcoming { + rawString = hours > 0 ? strings.Gift_Auction_StartsInHours : strings.Gift_Auction_StartsInMinutes + } else { + rawString = hours > 0 ? strings.Gift_Auction_TimeLeftHours : strings.Gift_Auction_TimeLeftMinutes + } + var buttonAnimatedTitleItems: [AnimatedTextComponent.Item] = [] var startIndex = rawString.startIndex while true { if let range = rawString.range(of: "{", range: startIndex ..< rawString.endIndex) { if range.lowerBound != startIndex { - buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix", content: .text(String(rawString[startIndex ..< range.lowerBound])))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "prefix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< range.lowerBound])))) } startIndex = range.upperBound @@ -874,7 +933,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } } if startIndex != rawString.endIndex { - buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix", content: .text(String(rawString[startIndex ..< rawString.endIndex])))) + buttonAnimatedTitleItems.append(AnimatedTextComponent.Item(id: "suffix_\(buttonAnimatedTitleItems.count)", content: .text(String(rawString[startIndex ..< rawString.endIndex])))) } let items: [AnyComponentWithIdentity] = [ diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index c0ad559a13..623ca2a7b7 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -2468,7 +2468,6 @@ private final class GiftViewSheetContent: CombinedComponent { let wearAvatar = Child(AvatarComponent.self) let wearPeerName = Child(MultilineTextComponent.self) - let wearPeerStatus = Child(MultilineTextComponent.self) let wearTitle = Child(MultilineTextComponent.self) let wearDescription = Child(MultilineTextComponent.self) let wearPerks = Child(List.self) @@ -2525,6 +2524,7 @@ private final class GiftViewSheetContent: CombinedComponent { var exported = false var canUpgrade = false var upgradeStars: Int64? + var genericGift: StarGift.Gift? var uniqueGift: StarGift.UniqueGift? var isSelfGift = false var isChannelGift = false @@ -2555,7 +2555,7 @@ private final class GiftViewSheetContent: CombinedComponent { subtitleString = strings.Gift_View_ReleasedBy("[@\(addressName)]()").string releasedByPeer = peer } - + genericGift = gift animationFile = gift.file stars = gift.price text = arguments.text @@ -2690,6 +2690,9 @@ private final class GiftViewSheetContent: CombinedComponent { } else if case let .upgradePreview(attributes, _) = component.subject { headerHeight = 258.0 headerSubject = .preview(attributes) + } else if case let .wearPreview(_, attributes) = component.subject, let attributes { + headerHeight = 200.0 + headerSubject = .preview(attributes) } else if let animationFile { headerHeight = 210.0 headerSubject = .generic(animationFile) @@ -2709,7 +2712,7 @@ private final class GiftViewSheetContent: CombinedComponent { let wearOwnerPeerId = ownerPeerId ?? component.context.account.peerId var wearPeerNameChild: _UpdatedChildComponent? - if showWearPreview, let uniqueGift { + if showWearPreview { var peerName = "" if let ownerPeer = state.peerMap[wearOwnerPeerId] { peerName = ownerPeer.displayTitle(strings: strings, displayOrder: nameDisplayOrder) @@ -2718,7 +2721,7 @@ private final class GiftViewSheetContent: CombinedComponent { component: MultilineTextComponent( text: .plain(NSAttributedString( string: peerName, - font: Font.bold(28.0), + font: Font.bold(20.0), textColor: .white, paragraphAlignment: .center )), @@ -2730,10 +2733,20 @@ private final class GiftViewSheetContent: CombinedComponent { ) let giftTitle: String - if case .wearPreview = component.subject { - giftTitle = uniqueGift.title + if let uniqueGift { + if case .wearPreview = component.subject { + giftTitle = uniqueGift.title + } else { + giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" + } + } else if let genericGift { + if let number = component.subject.arguments?.giftNumber { + giftTitle = "\(genericGift.title ?? "") #\(formatCollectibleNumber(number, dateTimeFormat: environment.dateTimeFormat))" + } else { + giftTitle = genericGift.title ?? "" + } } else { - giftTitle = "\(uniqueGift.title) #\(formatCollectibleNumber(uniqueGift.number, dateTimeFormat: environment.dateTimeFormat))" + giftTitle = "" } let wearTitle = wearTitle.update( @@ -2766,14 +2779,14 @@ private final class GiftViewSheetContent: CombinedComponent { transition: .immediate ) - var titleOriginY = headerHeight + 18.0 + var titleOriginY = headerHeight + 10.0 context.add(wearTitle .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearTitle.size.height)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) titleOriginY += wearTitle.size.height - titleOriginY += 18.0 + titleOriginY += 10.0 context.add(wearDescription .position(CGPoint(x: context.availableSize.width / 2.0, y: titleOriginY + wearDescription.size.height)) @@ -2785,7 +2798,7 @@ private final class GiftViewSheetContent: CombinedComponent { var animationOffset: CGPoint? var animationScale: CGFloat? if let wearPeerNameChild { - animationOffset = CGPoint(x: wearPeerNameChild.size.width / 2.0 + 20.0 - 12.0, y: 56.0) + animationOffset = CGPoint(x: wearPeerNameChild.size.width / 2.0 + 20.0 - 12.0, y: 79.0) animationScale = 0.19 } @@ -2848,42 +2861,21 @@ private final class GiftViewSheetContent: CombinedComponent { ) headerComponents.append({ context.add(wearAvatar - .position(CGPoint(x: context.availableSize.width / 2.0, y: 67.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 86.0)) .appear(.default(scale: true, alpha: true)) .disappear(.default(scale: true, alpha: true)) ) }) } - - let wearPeerStatus = wearPeerStatus.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: isChannelGift ? strings.Channel_Status : strings.Presence_online, - font: Font.regular(17.0), - textColor: vibrantColor, - paragraphAlignment: .center - )), - horizontalAlignment: .center, - maximumNumberOfLines: 5, - lineSpacing: 0.2 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 50.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - + headerComponents.append({ context.add(wearPeerNameChild - .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 144.0)) - .appear(.default(alpha: true)) - .disappear(.default(alpha: true)) - ) - context.add(wearPeerStatus - .position(CGPoint(x: context.availableSize.width / 2.0, y: 174.0)) + .position(CGPoint(x: context.availableSize.width / 2.0 - 12.0, y: 167.0)) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) }) - originY += 108.0 + originY += 91.0 let textColor = theme.actionSheet.primaryTextColor let secondaryTextColor = theme.actionSheet.secondaryTextColor @@ -5307,7 +5299,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { case profileGift(EnginePeer.Id, ProfileGiftsContext.State.StarGift) case soldOutGift(StarGift.Gift) case upgradePreview([StarGift.UniqueGift.Attribute], String) - case wearPreview(StarGift.UniqueGift) + case wearPreview(StarGift, [StarGift.UniqueGift.Attribute]?) var arguments: ( peerId: EnginePeer.Id?, @@ -5339,13 +5331,14 @@ public class GiftViewScreen: ViewControllerComponentContainer { prepaidUpgradeHash: String?, upgradeSeparate: Bool, dropOriginalDetailsStars: Int64?, - auctionToPeerId: EnginePeer.Id? + auctionToPeerId: EnginePeer.Id?, + giftNumber: Int32? )? { switch self { case let .message(message): if let action = message.media.first(where: { $0 is TelegramMediaAction }) as? TelegramMediaAction { switch action.action { - case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, _, toPeerId): + case let .starGift(gift, convertStars, text, entities, nameHidden, savedToProfile, converted, upgraded, canUpgrade, upgradeStars, isRefunded, _, upgradeMessageId, peerId, senderId, savedId, prepaidUpgradeHash, giftMessageId, upgradeSeparate, _, toPeerId, number): var reference: StarGiftReference if let peerId, let giftMessageId { reference = .message(messageId: EngineMessage.Id(peerId: peerId, namespace: Namespaces.Message.Cloud, id: giftMessageId)) @@ -5356,7 +5349,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { } let fromPeerId = senderId ?? message.author?.id - return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId) + return (message.id.peerId, fromPeerId, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, message.flags.contains(.Incoming), gift, message.timestamp, convertStars, text, entities, nameHidden, savedToProfile, nil, converted, upgraded, isRefunded, canUpgrade, upgradeStars, nil, nil, nil, upgradeMessageId, nil, nil, prepaidUpgradeHash, upgradeSeparate, nil, toPeerId, number) case let .starGiftUnique(gift, isUpgrade, isTransferred, savedToProfile, canExportDate, transferStars, _, _, peerId, senderId, savedId, _, canTransferDate, canResaleDate, dropOriginalDetailsStars, _, _): var reference: StarGiftReference if let peerId, let savedId { @@ -5378,13 +5371,15 @@ public class GiftViewScreen: ViewControllerComponentContainer { } var resellAmounts: [CurrencyAmount]? + var number: Int32? if case let .unique(uniqueGift) = gift { resellAmounts = uniqueGift.resellAmounts + number = uniqueGift.number } - return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars, nil) + return (message.id.peerId, senderId ?? message.author?.id, message.author?.debugDisplayTitle, message.author?.compactDisplayTitle, message.id, reference, incoming, gift, message.timestamp, nil, nil, nil, false, savedToProfile, nil, false, false, false, false, nil, transferStars, resellAmounts, canExportDate, nil, canTransferDate, canResaleDate, nil, false, dropOriginalDetailsStars, nil, number) case let .starGiftPurchaseOffer(gift, _, _, _, _), let .starGiftPurchaseOfferDeclined(gift, _, _): if case let .unique(gift) = gift { - return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil) + return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil, nil) } else { return nil } @@ -5392,8 +5387,8 @@ public class GiftViewScreen: ViewControllerComponentContainer { return nil } } - case let .uniqueGift(gift, _), let .wearPreview(gift): - return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil) + case let .uniqueGift(gift, _): + return (nil, nil, nil, nil, nil, nil, false, .unique(gift), 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, gift.resellAmounts, nil, nil, nil, nil, nil, false, nil, nil, gift.number) case let .profileGift(peerId, gift): var messageId: EngineMessage.Id? if case let .message(messageIdValue) = gift.reference { @@ -5403,11 +5398,13 @@ public class GiftViewScreen: ViewControllerComponentContainer { if case let .unique(uniqueGift) = gift.gift { resellAmounts = uniqueGift.resellAmounts } - return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars, nil) + return (peerId, gift.fromPeer?.id, gift.fromPeer?.debugDisplayTitle, gift.fromPeer?.compactDisplayTitle, messageId, gift.reference, false, gift.gift, gift.date, gift.convertStars, gift.text, gift.entities, gift.nameHidden, gift.savedToProfile, gift.pinnedToTop, false, false, false, gift.canUpgrade, gift.upgradeStars, gift.transferStars, resellAmounts, gift.canExportDate, nil, gift.canTransferDate, gift.canResaleDate, gift.prepaidUpgradeHash, gift.upgradeSeparate, gift.dropOriginalDetailsStars, nil, gift.number) case .soldOutGift: return nil case .upgradePreview: return nil + case let .wearPreview(gift, _): + return (nil, nil, nil, nil, nil, nil, false, gift, 0, nil, nil, nil, false, false, nil, false, false, false, false, nil, nil, nil, nil, nil, nil, nil, nil, false, nil, nil, nil) } return nil } @@ -6344,7 +6341,7 @@ private final class HeaderButtonComponent: CombinedComponent { } } -private final class AvatarComponent: Component { +final class AvatarComponent: Component { let context: AccountContext let theme: PresentationTheme let peer: EnginePeer diff --git a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift index be6a5431ad..f3918590f6 100644 --- a/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerNameColorScreen/Sources/UserApperanceScreen.swift @@ -774,6 +774,7 @@ final class UserAppearanceScreenComponent: Component { releasedBy: nil, valueAmount: nil, valueCurrency: nil, + valueUsdAmount: nil, flags: [], themePeerId: nil, peerColor: nil, diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 979044b141..4ce2ac80a9 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -3839,9 +3839,12 @@ public final class SharedAccountContextImpl: SharedAccountContext { return controller } - public func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift.UniqueGift) -> ViewController { - let controller = GiftViewScreen(context: context, subject: .wearPreview(gift)) - return controller + public func makeGiftWearPreviewScreen(context: AccountContext, gift: StarGift, attributes: [StarGift.UniqueGift.Attribute]?) -> ViewController { + return GiftViewScreen(context: context, subject: .wearPreview(gift, attributes)) + } + + public func makeGiftUpgradePreviewScreen(context: AccountContext, attributes: [StarGift.UniqueGift.Attribute], peerName: String) -> ViewController { + return GiftViewScreen(context: context, subject: .upgradePreview(attributes, peerName)) } public func makeGiftAuctionInfoScreen(context: AccountContext, auctionContext: GiftAuctionContext, completion: (() -> Void)?) -> ViewController { From 765aa49f41ef7edeec04d536b39fd091fb03e0d3 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 2 Dec 2025 17:01:07 +0800 Subject: [PATCH 08/11] Passkeys --- .../Components/ListActionItemComponent/BUILD | 1 + .../Sources/ContentContainer.swift | 260 ++++++++++++++++++ .../Sources/ListActionItemComponent.swift | 74 ++++- .../Sources/PasskeysScreen.swift | 7 + .../Sources/PasskeysScreenListComponent.swift | 52 +++- 5 files changed, 378 insertions(+), 16 deletions(-) create mode 100644 submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD index e9b7043eb5..c656c7429c 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/BUILD +++ b/submodules/TelegramUI/Components/ListActionItemComponent/BUILD @@ -16,6 +16,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/SwitchNode", "//submodules/CheckNode", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift new file mode 100644 index 0000000000..aa6362378b --- /dev/null +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift @@ -0,0 +1,260 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent + +private final class ContextOptionComponent: Component { + let title: String + let color: UIColor + let isLast: Bool + let action: () -> Void + + init( + title: String, + color: UIColor, + isLast: Bool, + action: @escaping () -> Void + ) { + self.title = title + self.color = color + self.isLast = isLast + self.action = action + } + + static func ==(lhs: ContextOptionComponent, rhs: ContextOptionComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.color != rhs.color { + return false + } + if lhs.isLast != rhs.isLast { + return false + } + return true + } + + final class View: UIView { + let backgroundView: UIView + let title = ComponentView() + + var component: ContextOptionComponent? + + override init(frame: CGRect) { + self.backgroundView = UIView() + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.component?.action() + } + } + + func update(component: ContextOptionComponent, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let sideInset: CGFloat = 8.0 + + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(17.0), textColor: .white)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) + ) + let size = CGSize(width: sideInset * 2.0 + titleSize.width, height: availableSize.height) + let titleFrame = CGRect(origin: CGPoint(x: sideInset, y: floorToScreenPixels((size.height - titleSize.height) * 0.5)), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + self.addSubview(titleView) + } + transition.setPosition(view: titleView, position: titleFrame.center) + titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size) + } + + self.backgroundView.backgroundColor = component.color + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width + (component.isLast ? 1000.0 : 0.0), height: size.height))) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: State, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +final class ContentContainer: UIScrollView, UIScrollViewDelegate { + private var itemViews: [AnyHashable: ComponentView] = [:] + + private var ignoreScrollingEvents: Bool = false + private var draggingBeganInClosedState: Bool = false + + private var contextOptions: [ListActionItemComponent.ContextOption] = [] + private var optionsWidth: CGFloat = 0.0 + + private var revealedStateTapRecognizer: UITapGestureRecognizer? + + override init(frame: CGRect) { + super.init(frame: frame) + + let revealedStateTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))) + self.revealedStateTapRecognizer = revealedStateTapRecognizer + revealedStateTapRecognizer.isEnabled = false + self.addGestureRecognizer(revealedStateTapRecognizer) + + self.delaysContentTouches = false + self.canCancelContentTouches = true + self.clipsToBounds = false + self.contentInsetAdjustmentBehavior = .never + self.automaticallyAdjustsScrollIndicatorInsets = false + self.showsVerticalScrollIndicator = false + self.showsHorizontalScrollIndicator = false + self.alwaysBounceHorizontal = false + self.alwaysBounceVertical = false + self.scrollsToTop = false + self.delegate = self + + self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let self else { + return false + } + + if self.contentOffset.x != 0.0 { + return true + } + + return false + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func onTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true) + } + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.revealedStateTapRecognizer?.isEnabled = self.contentOffset.x > 0.0 + } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.draggingBeganInClosedState = self.contentOffset.x == 0.0 + } + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + targetContentOffset.pointee.x = self.contentOffset.x + + if self.contentOffset.x >= self.optionsWidth + 30.0 { + self.contextOptions.last?.action() + } else { + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + if self.draggingBeganInClosedState { + if self.contentOffset.x > 20.0 { + self.setContentOffset(CGPoint(x: self.optionsWidth, y: 0.0), animated: true) + } else { + self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true) + } + } else { + if self.contentOffset.x < self.optionsWidth - 20.0 { + self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true) + } else { + self.setContentOffset(CGPoint(x: self.optionsWidth, y: 0.0), animated: true) + } + } + } + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let revealedStateTapRecognizer = self.revealedStateTapRecognizer, revealedStateTapRecognizer.isEnabled { + if self.bounds.contains(point), point.x < self.bounds.width { + return self + } + } + guard let result = super.hitTest(point, with: event) else { + return nil + } + return result + } + + func update(size: CGSize, contextOptions: [ListActionItemComponent.ContextOption], transition: ComponentTransition) { + self.contextOptions = contextOptions + + var validIds: [AnyHashable] = [] + var optionsWidth: CGFloat = 0.0 + for i in 0 ..< contextOptions.count { + let option = contextOptions[i] + validIds.append(option.id) + + let itemView: ComponentView + var itemTransition = transition + if let current = self.itemViews[option.id] { + itemView = current + } else { + itemTransition = itemTransition.withAnimation(.none) + itemView = ComponentView() + self.itemViews[option.id] = itemView + } + + let itemSize = itemView.update( + transition: itemTransition, + component: AnyComponent(ContextOptionComponent( + title: option.title, + color: option.color, + isLast: i == contextOptions.count - 1, + action: option.action + )), + environment: {}, + containerSize: CGSize(width: 10000.0, height: size.height) + ) + let itemFrame = CGRect(origin: CGPoint(x: size.width + optionsWidth, y: 0.0), size: itemSize) + optionsWidth += itemSize.width + if let itemComponentView = itemView.view { + self.addSubview(itemComponentView) + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) + } + } + var removedIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removedIds.append(id) + if let itemComponentView = itemView.view { + itemComponentView.removeFromSuperview() + } + } + } + for id in removedIds { + self.itemViews.removeValue(forKey: id) + } + self.optionsWidth = optionsWidth + + let contentSize = CGSize(width: size.width + optionsWidth, height: size.height) + if self.contentSize != contentSize { + self.contentSize = contentSize + } + self.isScrollEnabled = optionsWidth != 0.0 + } +} diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 05b33fcc04..e578f095b1 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -138,6 +138,33 @@ public final class ListActionItemComponent: Component { case center } + public final class ContextOption: Equatable { + public let id: AnyHashable + public let title: String + public let color: UIColor + public let action: () -> Void + + public init(id: AnyHashable, title: String, color: UIColor, action: @escaping () -> Void) { + self.id = id + self.title = title + self.color = color + self.action = action + } + + public static func ==(lhs: ContextOption, rhs: ContextOption) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.color != rhs.color { + return false + } + return true + } + } + public let theme: PresentationTheme public let style: Style public let background: AnyComponent? @@ -147,6 +174,7 @@ public final class ListActionItemComponent: Component { public let leftIcon: LeftIcon? public let icon: Icon? public let accessory: Accessory? + public let contextOptions: [ContextOption] public let action: ((UIView) -> Void)? public let highlighting: Highlighting public let updateIsHighlighted: ((UIView, Bool) -> Void)? @@ -161,6 +189,7 @@ public final class ListActionItemComponent: Component { leftIcon: LeftIcon? = nil, icon: Icon? = nil, accessory: Accessory? = .arrow, + contextOptions: [ContextOption] = [], action: ((UIView) -> Void)?, highlighting: Highlighting = .default, updateIsHighlighted: ((UIView, Bool) -> Void)? = nil @@ -174,6 +203,7 @@ public final class ListActionItemComponent: Component { self.leftIcon = leftIcon self.icon = icon self.accessory = accessory + self.contextOptions = contextOptions self.action = action self.highlighting = highlighting self.updateIsHighlighted = updateIsHighlighted @@ -207,6 +237,9 @@ public final class ListActionItemComponent: Component { if lhs.accessory != rhs.accessory { return false } + if lhs.contextOptions != rhs.contextOptions { + return false + } if (lhs.action == nil) != (rhs.action == nil) { return false } @@ -289,7 +322,9 @@ public final class ListActionItemComponent: Component { } } - public final class View: HighlightTrackingButton, ListSectionComponent.ChildView { + public final class View: UIView, ListSectionComponent.ChildView { + private let container: ContentContainer + private let button: HighlightTrackingButton private var background: ComponentView? private let title = ComponentView() private var leftIcon: ComponentView? @@ -316,10 +351,16 @@ public final class ListActionItemComponent: Component { public var separatorInset: CGFloat = 0.0 public override init(frame: CGRect) { + self.container = ContentContainer(frame: CGRect()) + self.button = HighlightTrackingButton() + super.init(frame: CGRect()) - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - self.internalHighligthedChanged = { [weak self] isHighlighted in + self.addSubview(self.container) + self.container.addSubview(self.button) + + self.button.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + self.button.internalHighligthedChanged = { [weak self] isHighlighted in guard let self, let component = self.component, component.action != nil else { return } @@ -479,7 +520,7 @@ public final class ListActionItemComponent: Component { let iconFrame = CGRect(origin: CGPoint(x: availableSize.width - contentRightInset - iconSize.width + iconOffset, y: floor((contentHeight - iconSize.height) * 0.5)), size: iconSize) if let iconView = icon.view { if iconView.superview == nil { - self.addSubview(iconView) + self.button.addSubview(iconView) transition.animateAlpha(view: iconView, from: 0.0, to: 1.0) } iconView.isUserInteractionEnabled = iconValue.allowUserInteraction @@ -516,7 +557,7 @@ public final class ListActionItemComponent: Component { animateIn = true leftCheckView = CheckView() self.leftCheckView = leftCheckView - self.addSubview(leftCheckView) + self.button.addSubview(leftCheckView) leftCheckView.action = { [weak self] in guard let self, let component = self.component else { @@ -596,7 +637,7 @@ public final class ListActionItemComponent: Component { if let leftIconView = leftIcon.view { if leftIconView.superview == nil { leftIconView.isUserInteractionEnabled = false - self.addSubview(leftIconView) + self.button.addSubview(leftIconView) transition.animateAlpha(view: leftIconView, from: 0.0, to: 1.0) } leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame) @@ -634,7 +675,7 @@ public final class ListActionItemComponent: Component { arrowTransition = arrowTransition.withAnimation(.none) arrowView = UIImageView(image: PresentationResourcesItemList.disclosureArrowImage(component.theme)) self.arrowView = arrowView - self.addSubview(arrowView) + self.button.addSubview(arrowView) } if let image = arrowView.image { @@ -660,7 +701,7 @@ public final class ListActionItemComponent: Component { arrowTransition = arrowTransition.withAnimation(.none) arrowView = UIImageView(image: PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme)) self.arrowView = arrowView - self.addSubview(arrowView) + self.button.addSubview(arrowView) } if let image = arrowView.image { @@ -689,7 +730,7 @@ public final class ListActionItemComponent: Component { switchNode = SwitchNode() switchNode.setOn(toggle.isOn, animated: false) self.switchNode = switchNode - self.addSubview(switchNode.view) + self.button.addSubview(switchNode.view) switchNode.valueUpdated = { [weak self] value in guard let self, let component = self.component else { @@ -739,7 +780,7 @@ public final class ListActionItemComponent: Component { switchNode.updateIsLocked(toggle.style == .lock) switchNode.setOn(toggle.isOn, animated: false) self.iconSwitchNode = switchNode - self.addSubview(switchNode.view) + self.button.addSubview(switchNode.view) switchNode.valueUpdated = { [weak self] value in guard let self, let component = self.component else { @@ -792,7 +833,7 @@ public final class ListActionItemComponent: Component { activityIndicatorView = UIActivityIndicatorView(style: .gray) } self.activityIndicatorView = activityIndicatorView - self.addSubview(activityIndicatorView) + self.button.addSubview(activityIndicatorView) activityIndicatorView.sizeToFit() } @@ -818,7 +859,7 @@ public final class ListActionItemComponent: Component { if let customAccessoryComponentView = customAccessoryView.view { if customAccessoryComponentView.superview == nil { customAccessoryComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) - self.addSubview(customAccessoryComponentView) + self.button.addSubview(customAccessoryComponentView) } customAccessoryComponentView.isUserInteractionEnabled = customAccessory.isInteractive customAccessoryTransition.setPosition(view: customAccessoryComponentView, position: CGPoint(x: activityAccessoryFrame.maxX, y: activityAccessoryFrame.minY)) @@ -835,7 +876,7 @@ public final class ListActionItemComponent: Component { if let titleView = self.title.view { if titleView.superview == nil { titleView.isUserInteractionEnabled = false - self.addSubview(titleView) + self.button.addSubview(titleView) } transition.setFrame(view: titleView, frame: titleFrame) } @@ -879,7 +920,12 @@ public final class ListActionItemComponent: Component { } } - return CGSize(width: availableSize.width, height: contentHeight) + let size = CGSize(width: availableSize.width, height: contentHeight) + self.container.update(size: size, contextOptions: component.contextOptions, transition: transition) + transition.setFrame(view: self.container, frame: CGRect(origin: CGPoint(), size: size)) + transition.setFrame(view: self.button, frame: CGRect(origin: CGPoint(), size: size)) + + return size } } diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift index bedfb253ea..06d143f3de 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreen.swift @@ -299,8 +299,15 @@ final class PasskeysScreenComponent: Component { component: AnyComponent(PasskeysScreenListComponent( context: component.context, theme: environment.theme, + strings: environment.strings, insets: UIEdgeInsets(top: environment.statusBarHeight, left: 0.0, bottom: environment.safeInsets.bottom, right: 0.0), passkeys: passkeysData, + addPasskeyAction: { [weak self] in + guard let self else { + return + } + self.createPasskey() + }, deletePasskeyAction: { [weak self] id in guard let self else { return diff --git a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift index dfafe38578..862e6600ed 100644 --- a/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift +++ b/submodules/TelegramUI/Components/Settings/PasskeysScreen/Sources/PasskeysScreenListComponent.swift @@ -17,21 +17,27 @@ import EmojiStatusComponent final class PasskeysScreenListComponent: Component { let context: AccountContext let theme: PresentationTheme + let strings: PresentationStrings let insets: UIEdgeInsets let passkeys: [TelegramPasskey] + let addPasskeyAction: () -> Void let deletePasskeyAction: (String) -> Void init( context: AccountContext, theme: PresentationTheme, + strings: PresentationStrings, insets: UIEdgeInsets, passkeys: [TelegramPasskey], + addPasskeyAction: @escaping () -> Void, deletePasskeyAction: @escaping (String) -> Void ) { self.context = context self.theme = theme + self.strings = strings self.insets = insets self.passkeys = passkeys + self.addPasskeyAction = addPasskeyAction self.deletePasskeyAction = deletePasskeyAction } @@ -42,6 +48,9 @@ final class PasskeysScreenListComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.strings !== rhs.strings { + return false + } if lhs.insets != rhs.insets { return false } @@ -101,6 +110,11 @@ final class PasskeysScreenListComponent: Component { self.component = component self.state = state + var maxPasskeys = 5 + if let data = component.context.currentAppConfiguration.with({ $0 }).data, let maxValue = data["passkeys_account_passkeys_max"] as? Double { + maxPasskeys = Int(maxValue) + } + self.backgroundColor = component.theme.list.blocksBackgroundColor let sideInset: CGFloat = 16.0 + component.insets.left @@ -227,7 +241,7 @@ final class PasskeysScreenListComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: passkey.name, + string: passkey.name.isEmpty ? "Passkey" : passkey.name, //TODO:localize font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor )), @@ -247,11 +261,45 @@ final class PasskeysScreenListComponent: Component { false ), accessory: nil, + contextOptions: [ListActionItemComponent.ContextOption( + id: "delete", + title: component.strings.Common_Delete, + color: component.theme.list.itemDisclosureActions.destructive.fillColor, + action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.deletePasskeyAction(passkeyId) + } + )], + action: nil, + highlighting: .default + )))) + } + + if component.passkeys.count < maxPasskeys { + listSectionItems.append(AnyComponentWithIdentity(id: "_add", component: AnyComponent(ListActionItemComponent( + theme: component.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Create Passkey", //TODO:localize + font: Font.regular(17.0), + textColor: component.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))) + ], alignment: .left, spacing: 2.0)), + leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Chat List/AddIcon", + tintColor: component.theme.list.itemAccentColor + ))), false), + accessory: nil, action: { [weak self] _ in guard let self, let component = self.component else { return } - component.deletePasskeyAction(passkeyId) + component.addPasskeyAction() }, highlighting: .default )))) From 50fe471cb683e9828b4ef792b73f32ad660f7067 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 2 Dec 2025 13:02:06 +0400 Subject: [PATCH 09/11] Show extended auction round info --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 +++ .../Sources/GiftAuctionViewScreen.swift | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0531fec09e..2a04aab41e 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15494,9 +15494,12 @@ Error: %8$@"; "Gift.Auction.Hours_any" = "%@ hours"; "Gift.Auction.Minutes_1" = "%@ minute"; "Gift.Auction.Minutes_any" = "%@ minutes"; +"Gift.Auction.Seconds_1" = "%@ second"; +"Gift.Auction.Seconds_any" = "%@ seconds"; "Gift.Auction.HoursEach_1" = "%@ hour each"; "Gift.Auction.HoursEach_any" = "%@ hours each"; "Gift.Auction.MinutesEach_1" = "%@ minute each"; "Gift.Auction.MinutesEach_any" = "%@ minutes each"; +"Gift.Auction.Extension" = "+ %1$@ for late bids in top %2$@"; "ChatList.Auctions.UpcomingAuction" = "Upcoming Auction"; diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift index 7688e1b171..d752338fb4 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftAuctionViewScreen.swift @@ -620,7 +620,7 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { } let title: String = start == end ? strings.Gift_Auction_TimeRound("\(start)").string : strings.Gift_Auction_TimeRounds("\(start)-\(end)").string - let value: String + var value: String if round.duration % 3600 == 0 { let hours = round.duration / 3600 value = start == end ? strings.Gift_Auction_Hours(hours) : strings.Gift_Auction_HoursEach(hours) @@ -629,11 +629,24 @@ private final class GiftAuctionViewSheetContent: CombinedComponent { value = start == end ? strings.Gift_Auction_Minutes(minutes) : strings.Gift_Auction_MinutesEach(minutes) } + if case let .extendable(_, _, top, window) = round { + var windowString: String + if window % 60 == 0 { + windowString = strings.Gift_Auction_Minutes(window) + } else { + windowString = strings.Gift_Auction_Seconds(window) + } + value += " \(strings.Gift_Auction_Extension(windowString, "\(top)").string)" + } + tableItems.append(.init( id: "round_\(i)", title: title, component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: value, font: tableFont, textColor: tableTextColor))) + MultilineTextComponent( + text: .plain(NSAttributedString(string: value, font: tableFont, textColor: tableTextColor)), + maximumNumberOfLines: 3 + ) ) )) } From cf5584e02ff15a1e9317c6977394cd01dacbc5a8 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 2 Dec 2025 17:20:43 +0800 Subject: [PATCH 10/11] Collapse sibling options --- .../AdminUserActionsPeerComponent.swift | 1 + .../Sources/ContentContainer.swift | 20 +++++++++++++++++-- .../Sources/ListActionItemComponent.swift | 20 ++++++++++++++++++- .../ListComposePollOptionComponent.swift | 1 + .../ListItemSliderSelectorComponent.swift | 1 + .../ListMultilineTextFieldItemComponent.swift | 1 + .../Sources/ListSectionComponent.swift | 12 +++++++++++ .../Sources/ListTextFieldItemComponent.swift | 1 + .../Sources/HashtagListItemComponent.swift | 1 + ...aticBusinessMessageListItemComponent.swift | 1 + .../BusinessLinkListItemComponent.swift | 1 + .../Sources/ChatIntroItemComponent.swift | 1 + .../Sources/MapPreviewComponent.swift | 1 + .../ChatbotSearchResultItemComponent.swift | 1 + .../Sources/PeerListItemComponent.swift | 1 + 15 files changed, 61 insertions(+), 3 deletions(-) diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift index 69bab1e9b0..dc1f55617c 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsPeerComponent.swift @@ -108,6 +108,7 @@ final class AdminUserActionsPeerComponent: Component { private weak var state: EmptyComponentState? public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public var separatorInset: CGFloat = 0.0 override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift index aa6362378b..2fd83b0272 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ContentContainer.swift @@ -100,18 +100,23 @@ private final class ContextOptionComponent: Component { } final class ContentContainer: UIScrollView, UIScrollViewDelegate { + private let closeOtherContextOptions: () -> Void + private var itemViews: [AnyHashable: ComponentView] = [:] private var ignoreScrollingEvents: Bool = false private var draggingBeganInClosedState: Bool = false + private var didProcessScrollingCycle: Bool = false private var contextOptions: [ListActionItemComponent.ContextOption] = [] private var optionsWidth: CGFloat = 0.0 private var revealedStateTapRecognizer: UITapGestureRecognizer? - override init(frame: CGRect) { - super.init(frame: frame) + init(closeOtherContextOptions: @escaping () -> Void) { + self.closeOtherContextOptions = closeOtherContextOptions + + super.init(frame: CGRect()) let revealedStateTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:))) self.revealedStateTapRecognizer = revealedStateTapRecognizer @@ -155,9 +160,16 @@ final class ContentContainer: UIScrollView, UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { self.revealedStateTapRecognizer?.isEnabled = self.contentOffset.x > 0.0 + if self.contentOffset.x > 0.0 { + if !self.didProcessScrollingCycle { + self.didProcessScrollingCycle = true + self.closeOtherContextOptions() + } + } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.didProcessScrollingCycle = false self.draggingBeganInClosedState = self.contentOffset.x == 0.0 } @@ -200,6 +212,10 @@ final class ContentContainer: UIScrollView, UIScrollViewDelegate { return result } + func closeContextOptions() { + self.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: true) + } + func update(size: CGSize, contextOptions: [ListActionItemComponent.ContextOption], transition: ComponentTransition) { self.contextOptions = contextOptions diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index e578f095b1..435802b919 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -348,10 +348,14 @@ public final class ListActionItemComponent: Component { } public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public var separatorInset: CGFloat = 0.0 public override init(frame: CGRect) { - self.container = ContentContainer(frame: CGRect()) + var closeOtherContextOptions: (() -> Void)? + self.container = ContentContainer(closeOtherContextOptions: { + closeOtherContextOptions?() + }) self.button = HighlightTrackingButton() super.init(frame: CGRect()) @@ -375,6 +379,20 @@ public final class ListActionItemComponent: Component { customUpdateIsHighlighted(isHighlighted) } } + + closeOtherContextOptions = { [weak self] in + guard let self else { + return + } + self.enumerateSiblings?({ sibling in + if self === sibling { + return + } + if let view = sibling as? View { + view.container.closeContextOptions() + } + }) + } } required public init?(coder: NSCoder) { diff --git a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift index f0784bd4a2..0744f141da 100644 --- a/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift +++ b/submodules/TelegramUI/Components/ListComposePollOptionComponent/Sources/ListComposePollOptionComponent.swift @@ -425,6 +425,7 @@ public final class ListComposePollOptionComponent: Component { } public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public private(set) var separatorInset: CGFloat = 0.0 public override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift index 31cd8ad9a1..a7ec0fe0da 100644 --- a/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift +++ b/submodules/TelegramUI/Components/ListItemSliderSelectorComponent/Sources/ListItemSliderSelectorComponent.swift @@ -130,6 +130,7 @@ public final class ListItemSliderSelectorComponent: Component { private weak var state: EmptyComponentState? public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public private(set) var separatorInset: CGFloat = 0.0 override public init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift index 60dc69c6bd..171a610f85 100644 --- a/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent/Sources/ListMultilineTextFieldItemComponent.swift @@ -202,6 +202,7 @@ public final class ListMultilineTextFieldItemComponent: Component { } public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public private(set) var separatorInset: CGFloat = 0.0 public override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift index 10ad6baf5d..9b6d022b7e 100644 --- a/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift +++ b/submodules/TelegramUI/Components/ListSectionComponent/Sources/ListSectionComponent.swift @@ -7,6 +7,7 @@ import DynamicCornerRadiusView public protocol ListSectionComponentChildView: AnyObject { var customUpdateIsHighlighted: ((Bool) -> Void)? { get set } + var enumerateSiblings: (((UIView) -> Void) -> Void)? { get set } var separatorInset: CGFloat { get } } @@ -190,6 +191,16 @@ public final class ListSectionContentView: UIView { } self.updateHighlightedItem(itemId: isHighlighted ? itemId : nil) } + itemComponentView.enumerateSiblings = { [weak self, weak itemComponentView] f in + guard let self, let itemComponentView else { + return + } + for (_, itemView) in self.itemViews { + if let otherItemView = itemView.contents.view, otherItemView !== itemComponentView { + f(otherItemView) + } + } + } } } var separatorInset: CGFloat = 0.0 @@ -619,6 +630,7 @@ public final class ListSubSectionComponent: Component { private var component: ListSubSectionComponent? public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public var separatorInset: CGFloat = 0.0 public override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift index ae8554b1e3..dbe60f2178 100644 --- a/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift +++ b/submodules/TelegramUI/Components/ListTextFieldItemComponent/Sources/ListTextFieldItemComponent.swift @@ -127,6 +127,7 @@ public final class ListTextFieldItemComponent: Component { } public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public private(set) var separatorInset: CGFloat = 0.0 public override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift index e51ab6cfea..13cfb235cb 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/HashtagListItemComponent.swift @@ -196,6 +196,7 @@ public final class HashtagListItemComponent: Component { private var isExtractedToContextMenu: Bool = false public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public private(set) var separatorInset: CGFloat = 0.0 override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift index c293113855..74bcf76ef4 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageListItemComponent.swift @@ -72,6 +72,7 @@ final class GreetingMessageListItemComponent: Component { private var itemNode: ListViewItemNode? var customUpdateIsHighlighted: ((Bool) -> Void)? + var enumerateSiblings: (((UIView) -> Void) -> Void)? private(set) var separatorInset: CGFloat = 0.0 override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift index 16d7b4488f..e1f7fd4b1f 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkListItemComponent.swift @@ -73,6 +73,7 @@ final class BusinessLinkListItemComponent: Component { private weak var componentState: EmptyComponentState? var customUpdateIsHighlighted: ((Bool) -> Void)? + var enumerateSiblings: (((UIView) -> Void) -> Void)? private(set) var separatorInset: CGFloat = 0.0 private var isExtractedToContextMenu: Bool = false diff --git a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift index a6811b04db..1bd282be5d 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessIntroSetupScreen/Sources/ChatIntroItemComponent.swift @@ -67,6 +67,7 @@ final class ChatIntroItemComponent: Component { private var emptyNode: ChatEmptyNode? var customUpdateIsHighlighted: ((Bool) -> Void)? + var enumerateSiblings: (((UIView) -> Void) -> Void)? private(set) var separatorInset: CGFloat = 0.0 override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift index b48375c117..35f16ff3bd 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessLocationSetupScreen/Sources/MapPreviewComponent.swift @@ -56,6 +56,7 @@ final class MapPreviewComponent: Component { private let pinForegroundView: UIImageView var customUpdateIsHighlighted: ((Bool) -> Void)? + var enumerateSiblings: (((UIView) -> Void) -> Void)? private(set) var separatorInset: CGFloat = 0.0 override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift index 3423eb4caf..bbe37bf01b 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSearchResultItemComponent.swift @@ -74,6 +74,7 @@ final class ChatbotSearchResultItemComponent: Component { private weak var state: EmptyComponentState? var customUpdateIsHighlighted: ((Bool) -> Void)? + var enumerateSiblings: (((UIView) -> Void) -> Void)? private(set) var separatorInset: CGFloat = 0.0 override init(frame: CGRect) { diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index 6567c1d2a5..3e4d303edd 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -464,6 +464,7 @@ public final class PeerListItemComponent: Component { private var isExtractedToContextMenu: Bool = false public var customUpdateIsHighlighted: ((Bool) -> Void)? + public var enumerateSiblings: (((UIView) -> Void) -> Void)? public private(set) var separatorInset: CGFloat = 0.0 override init(frame: CGRect) { From 8047d0fa2affa283f3fa90977a9adf5a0036eda7 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 2 Dec 2025 17:30:30 +0800 Subject: [PATCH 11/11] Revert "Revert "[WIP] Tab bar and search"" This reverts commit 89f96e28b3c4a36177b50dd0d2978cfb66b6d85c. --- Telegram/BUILD | 3 - .../Sources/CallListController.swift | 2 + submodules/ChatListUI/BUILD | 1 + .../Sources/ChatListController.swift | 136 ++-- .../Sources/ChatListControllerNode.swift | 15 +- .../Sources/Node/ChatListItem.swift | 20 +- .../Source/Base/Transition.swift | 35 + .../Sources/ContactsController.swift | 2 + .../Sources/ContactsControllerNode.swift | 3 +- .../Display/Source/ViewController.swift | 25 + .../SearchBarNode/Sources/SearchBarNode.swift | 91 ++- .../Sources/SearchDisplayController.swift | 189 ++++-- .../Sources/TabBarContollerNode.swift | 91 ++- .../TabBarUI/Sources/TabBarController.swift | 37 +- .../Sources/ChatTextInputPanelNode.swift | 3 + .../ChatListFilterTabContainerNode/BUILD | 26 + .../ChatListFilterTabContainerNode.swift | 55 +- .../Components/ChatListHeaderComponent/BUILD | 2 + .../Sources/ChatListHeaderComponent.swift | 627 ++++++------------ .../Sources/ChatListNavigationBar.swift | 184 ++--- .../Sources/NavigationButtonComponent.swift | 254 +++++++ .../NavigationButtonContainerView.swift | 6 + .../Sources/ChatListTitleProxyNode.swift | 18 +- .../LiquidLens/Sources/LiquidLensView.swift | 79 ++- .../Sources/MiniAppListScreen.swift | 5 +- .../Sources/PeerInfoScreen.swift | 10 +- .../Components/PeerSelectionController/BUILD | 1 + .../Sources/PeerSelectionController.swift | 1 + .../Sources/QuickReplySetupScreen.swift | 5 +- .../Sources/PeerSelectionScreen.swift | 5 +- .../Components/TabBarComponent/BUILD | 1 + .../Sources/TabBarComponent.swift | 356 ++++++++-- .../Sources/TelegramRootController.swift | 2 +- 33 files changed, 1474 insertions(+), 816 deletions(-) create mode 100644 submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD rename submodules/{ChatListUI => TelegramUI/Components/ChatList/ChatListFilterTabContainerNode}/Sources/ChatListFilterTabContainerNode.swift (96%) create mode 100644 submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift create mode 100644 submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift diff --git a/Telegram/BUILD b/Telegram/BUILD index 74b3ecae94..c6ebd55366 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -8,8 +8,6 @@ load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application", "ios_extension", "ios_framework", - "ios_unit_test", - "ios_ui_test", ) load("@build_bazel_rules_apple//apple:resources.bzl", @@ -22,7 +20,6 @@ load("@build_bazel_rules_swift//swift:swift.bzl", load( "@rules_xcodeproj//xcodeproj:defs.bzl", - "top_level_target", "top_level_targets", "xcodeproj", "xcode_provisioning_profile", diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index b8e54aecd3..06bfd50c46 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -155,6 +155,8 @@ public final class CallListController: TelegramBaseController { if case .navigation = self.mode { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index a28100bbf7..47492d7859 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -118,6 +118,7 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5c5e8f23e3..078854b0e6 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -55,6 +55,8 @@ import TextFormat import AvatarUploadToastScreen import AdsInfoScreen import AdsReportScreen +import SearchBarNode +import ChatListFilterTabContainerNode private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController @@ -768,6 +770,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) self.updateNavigationMetadata() + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { @@ -2838,14 +2842,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { var primaryContent: ChatListHeaderComponent.Content? if let primaryContext = self.primaryContext { - var backTitle: String? - if let previousItem = self.previousItem { - switch previousItem { - case let .item(item): - backTitle = item.title ?? self.presentationData.strings.Common_Back - case .close: - backTitle = self.presentationData.strings.Common_Close - } + var displayBackButton: Bool = false + if self.previousItem != nil { + displayBackButton = true } var navigationBackTitle: String? if case .chatList(.archive) = self.location { @@ -2858,8 +2857,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatListTitle: primaryContext.chatListTitle, leftButton: primaryContext.leftButton, rightButtons: primaryContext.rightButtons, - backTitle: backTitle, - backPressed: backTitle != nil ? { [weak self] in + backPressed: displayBackButton ? { [weak self] in guard let self else { return } @@ -2876,7 +2874,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatListTitle: secondaryContext.chatListTitle, leftButton: secondaryContext.leftButton, rightButtons: secondaryContext.rightButtons, - backTitle: nil, backPressed: { [weak self] in guard let self else { return @@ -4638,9 +4635,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController searchContentNode = navigationBarView.searchContentNode } - if let searchContentNode { - self.activateSearch(filter: filter, query: query, skipScrolling: false, searchContentNode: searchContentNode) - } + self.activateSearch(filter: filter, query: query, skipScrolling: false, searchContentNode: searchContentNode) } public func activateSearch(query: String? = nil) { @@ -4654,45 +4649,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private var previousSearchToggleTimestamp: Double? - func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false, searchContentNode: NavigationBarSearchContentNode) { - let currentTimestamp = CACurrentMediaTime() - if let previousSearchActivationTimestamp = self.previousSearchToggleTimestamp, currentTimestamp < previousSearchActivationTimestamp + 0.6 { - return - } - self.previousSearchToggleTimestamp = currentTimestamp - - if let storyTooltip = self.storyTooltip { - storyTooltip.dismiss() - } - - var filter = filter - if case .forum = self.chatListDisplayNode.effectiveContainerNode.location { - filter = .topics - } - - if self.chatListDisplayNode.searchDisplayController == nil { - /*if !skipScrolling, let searchContentNode = self.searchContentNode, searchContentNode.expansionProgress != 1.0 { - self.scrollToTop?() - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in - self?.activateSearch(filter: filter, query: query, skipScrolling: true) - }) + func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false, searchContentNode: NavigationBarSearchContentNode?) { + Task { @MainActor [weak self] in + guard let self else { return - }*/ - //TODO:scroll to top? + } + + let currentTimestamp = CACurrentMediaTime() + if let previousSearchActivationTimestamp = self.previousSearchToggleTimestamp, currentTimestamp < previousSearchActivationTimestamp + 0.6 { + return + } + self.previousSearchToggleTimestamp = currentTimestamp - let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _, chatListView in - Task { @MainActor in - guard let strongSelf = self else { - return - } - + if let storyTooltip = self.storyTooltip { + storyTooltip.dismiss() + } + + var filter = filter + if case .forum = self.chatListDisplayNode.effectiveContainerNode.location { + filter = .topics + } + + if self.chatListDisplayNode.searchDisplayController == nil { + let (_, _) = await combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)).get() + + do { /*if let scrollToTop = strongSelf.scrollToTop { - scrollToTop() - }*/ + scrollToTop() + }*/ let tabsIsEmpty: Bool - if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData { + if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData { tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom } else { tabsIsEmpty = true @@ -4702,40 +4689,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let displaySearchFilters = true - if let filterContainerNodeAndActivate = await strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) { + if let filterContainerNodeAndActivate = await self.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode?.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: self.hasDownloads, initialFilter: filter, navigationController: self.navigationController as? NavigationController, searchBarIsExternal: true) { let (filterContainerNode, activate) = filterContainerNodeAndActivate if displaySearchFilters { let searchTabsNode = SparseNode() - strongSelf.searchTabsNode = searchTabsNode + self.searchTabsNode = searchTabsNode searchTabsNode.addSubnode(filterContainerNode) } activate(filter != .downloads) - if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { searchContentNode.search(filter: filter, query: query) } } let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - strongSelf.setDisplayNavigationBar(false, transition: transition) - - (strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring)) - } - }) - - self.isSearchActive = true - if let navigationController = self.navigationController as? NavigationController { - for controller in navigationController.globalOverlayControllers { - if let controller = controller as? VoiceChatOverlayController { - controller.updateVisibility() - break + self.setDisplayNavigationBar(false, transition: transition) + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: true), transition: transition) + if let searchBarNode = self.currentTabBarSearchNode?() as? SearchBarNode { + self.chatListDisplayNode.searchDisplayController?.setSearchBar(searchBarNode) + searchBarNode.activate() + } + + self.isSearchActive = true + if let navigationController = self.navigationController as? NavigationController { + for controller in navigationController.globalOverlayControllers { + if let controller = controller as? VoiceChatOverlayController { + controller.updateVisibility() + break + } + } } } - } - } else if self.isSearchActive { - if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { - searchContentNode.search(filter: filter, query: query) + } else if self.isSearchActive { + if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + searchContentNode.search(filter: filter, query: query) + } } } } @@ -4766,6 +4756,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated) searchContentNode.placeholderNode.frame = previousFrame + } else { + completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: nil, animated: animated) } self.chatListDisplayNode.tempAllowAvatarExpansion = true @@ -4780,7 +4772,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController completion?() - (self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring)) + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: transition) self.isSearchActive = false if let navigationController = self.navigationController as? NavigationController { @@ -6236,6 +6228,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } + + override public func tabBarActivateSearch() { + self.activateSearch() + } + + override public func tabBarDeactivateSearch() { + self.deactivateSearch(animated: true) + } private var playedSignUpCompletedAnimation = false public func playSignUpCompletedAnimation() { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 3a8357317e..9b4a72354e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -22,6 +22,7 @@ import ChatListHeaderComponent import StoryPeerListComponent import TelegramNotices import EdgeEffect +import ChatListFilterTabContainerNode public enum ChatListContainerNodeFilter: Equatable { case all @@ -1382,8 +1383,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, + search: nil, //ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: headerContent?.primaryContent, secondaryContent: headerContent?.secondaryContent, secondaryTransition: self.inlineStackContainerTransitionFraction, @@ -1540,7 +1541,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var storiesInset = storiesInset let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: ComponentTransition(transition)) - self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset + //self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset + self.mainContainerNode.initialScrollingOffset = navigationBarLayout.storiesInset navigationBarHeight = navigationBarLayout.navigationHeight visualNavigationHeight = navigationBarLayout.navigationHeight @@ -1666,7 +1668,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } @MainActor - func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) async -> (ASDisplayNode, (Bool) -> Void)? { + func activateSearch(placeholderNode: SearchBarPlaceholderNode?, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?, searchBarIsExternal: Bool) async -> (ASDisplayNode, (Bool) -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else { return nil } @@ -1712,7 +1714,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() } - }) + }, searchBarIsExternal: searchBarIsExternal) self.mainContainerNode.accessibilityElementsHidden = true self.inlineStackContainerNode?.accessibilityElementsHidden = true @@ -1742,7 +1744,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { }) } - func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) -> (() -> Void)? { + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode?, animated: Bool) -> (() -> Void)? { if let searchDisplayController = self.searchDisplayController { self.isSearchDisplayControllerActive = false self.searchDisplayController = nil @@ -1750,7 +1752,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.inlineStackContainerNode?.accessibilityElementsHidden = false return { [weak self, weak placeholderNode] in - if let strongSelf = self, let placeholderNode, let (layout, _, _, cleanNavigationBarHeight, _) = strongSelf.containerLayout { + if let strongSelf = self, let (layout, _, _, cleanNavigationBarHeight, _) = strongSelf.containerLayout { + let placeholderNode = placeholderNode searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated) searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 46974fd940..9b445ccc3f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2103,7 +2103,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let effectiveBackgroundColor: UIColor if item.isPinned { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat) - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat) effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor @@ -2117,7 +2119,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let avatarLiveBadge = self.avatarLiveBadge { let effectiveBackgroundColor: UIColor if item.isPinned { - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } @@ -4127,7 +4131,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let effectiveBackgroundColor: UIColor if item.isPinned { - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } @@ -4148,7 +4154,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { effectiveBackgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor } else if case let .chatList(index) = item.index, index.pinningIndex != nil { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat) - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat) effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor @@ -5060,7 +5068,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { backgroundColor = theme.itemBackgroundColor highlightedBackgroundColor = theme.itemHighlightedBackgroundColor } else { - backgroundColor = theme.pinnedItemBackgroundColor + //TODO:localize + backgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //backgroundColor = theme.pinnedItemBackgroundColor highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor } } else { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 4dcd0401d2..ae393778c4 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1335,6 +1335,41 @@ public struct ComponentTransition { completion: completion ) } + + public func setBlur(layer: CALayer, radius: CGFloat, completion: ((Bool) -> Void)? = nil) { + var currentRadius: CGFloat = 0.0 + if let currentFilters = layer.filters { + for filter in currentFilters { + if let filter = filter as? NSObject, filter.description.contains("gaussianBlur") { + currentRadius = filter.value(forKey: "inputRadius") as? CGFloat ?? 0.0 + } + } + } + + if currentRadius == radius { + completion?(true) + return + } + + if let blurFilter = CALayer.blur() { + blurFilter.setValue(radius as NSNumber, forKey: "inputRadius") + layer.filters = [blurFilter] + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + layer.animate(from: currentRadius as NSNumber, to: radius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", duration: duration, delay: 0.0, curve: curve, removeOnCompletion: true, additive: false,completion: { [weak layer] flag in + if let layer { + if radius <= 0.0 { + layer.filters = nil + } + } + + completion?(flag) + }) + } + } + } public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { let duration: Double diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 425ca2b7ab..e75897f583 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -226,6 +226,8 @@ public class ContactsController: ViewController { } self.sortButton.addTarget(self, action: #selector(self.sortPressed), forControlEvents: .touchUpInside) + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 2718f81f91..bd1e967a3b 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -350,7 +350,6 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { chatListTitle: NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil), leftButton: leftButton, rightButtons: rightButtons, - backTitle: nil, backPressed: nil ) @@ -362,8 +361,8 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, + search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: primaryContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 593005a7e5..f81403f2dd 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -217,6 +217,18 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open var navigationBarRequiresEntireLayoutUpdate: Bool { return true } + + public struct TabBarSearchState: Equatable { + public var isActive: Bool + + public init(isActive: Bool) { + self.isActive = isActive + } + } + + public private(set) var tabBarSearchState: TabBarSearchState? + public var tabBarSearchStateUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var currentTabBarSearchNode: (() -> ASDisplayNode?)? private weak var activeInputViewCandidate: UIResponder? private weak var activeInputView: UIResponder? @@ -705,9 +717,22 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open func tabBarDisabledAction() { } + + open func tabBarActivateSearch() { + } + + open func tabBarDeactivateSearch() { + } open func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) { } + + public func updateTabBarSearchState(_ tabBarSearchState: TabBarSearchState?, transition: ContainedViewLayoutTransition) { + if self.tabBarSearchState != tabBarSearchState { + self.tabBarSearchState = tabBarSearchState + self.tabBarSearchStateUpdated?(transition) + } + } open func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) { } diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 131323d318..773e1f4259 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -369,6 +369,7 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { } var theme: SearchBarNodeTheme + let style: SearchBarStyle fileprivate func layoutTokens(transition: ContainedViewLayoutTransition = .immediate) { var hasSelected = false @@ -519,8 +520,9 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { } } - init(theme: SearchBarNodeTheme) { + init(theme: SearchBarNodeTheme, style: SearchBarStyle) { self.theme = theme + self.style = style self.placeholderLabel = ImmediateTextNode() self.placeholderLabel.isUserInteractionEnabled = false @@ -679,6 +681,10 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { placeholderYOffset = 1.0 } else { } + + if case .inlineNavigation = self.style { + placeholderYOffset += 6.0 + } let textRect = self.textRect(forBounds: bounds) let labelSize = self.placeholderLabel.updateLayout(textRect.size) @@ -813,40 +819,45 @@ public final class SearchBarNodeTheme: Equatable { public enum SearchBarStyle { case modern case legacy + case inlineNavigation var font: UIFont { switch self { - case .modern: - return Font.regular(17.0) - case .legacy: - return Font.regular(14.0) + case .modern, .inlineNavigation: + return Font.regular(17.0) + case .legacy: + return Font.regular(14.0) } } var cornerDiameter: CGFloat { switch self { - case .modern: - return 21.0 - case .legacy: - return 14.0 + case .modern, .inlineNavigation: + return 21.0 + case .legacy: + return 14.0 } } var height: CGFloat { switch self { - case .modern: - return 36.0 - case .legacy: - return 28.0 + case .inlineNavigation: + return 48.0 + case .modern: + return 36.0 + case .legacy: + return 28.0 } } var padding: CGFloat { switch self { - case .modern: - return 10.0 - case .legacy: - return 8.0 + case .inlineNavigation: + return 0.0 + case .modern: + return 10.0 + case .legacy: + return 8.0 } } } @@ -994,7 +1005,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true - self.textField = SearchBarTextField(theme: theme) + self.textField = SearchBarTextField(theme: theme, style: fieldStyle) self.textField.accessibilityTraits = .searchField self.textField.autocorrectionType = .no self.textField.returnKeyType = .search @@ -1011,14 +1022,21 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - - self.addSubnode(self.textBackgroundNode) + if case .inlineNavigation = self.fieldStyle { + } else { + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.textBackgroundNode) + } self.view.addSubview(self.textField) - self.addSubnode(self.iconNode) + + if case .inlineNavigation = self.fieldStyle { + } else { + self.addSubnode(self.iconNode) + self.addSubnode(self.cancelButton) + } + self.addSubnode(self.clearButton) - self.addSubnode(self.cancelButton) self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) @@ -1093,19 +1111,34 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: .immediate) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))) - let verticalOffset: CGFloat = boundingSize.height - 82.0 - let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height)) - let textBackgroundHeight = self.fieldStyle.height + let textBackgroundHeight: CGFloat + if case .inlineNavigation = self.fieldStyle { + textBackgroundHeight = boundingSize.height + } else { + textBackgroundHeight = self.fieldStyle.height + } + let verticalOffset: CGFloat + if case .inlineNavigation = self.fieldStyle { + verticalOffset = -textBackgroundHeight + } else { + verticalOffset = boundingSize.height - 82.0 + } let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity)) transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 10.0 - cancelButtonSize.width, y: verticalOffset + textBackgroundHeight + floorToScreenPixels((textBackgroundHeight - cancelButtonSize.height) / 2.0)), size: cancelButtonSize)) let padding = self.fieldStyle.padding - let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) + var textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) - let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 24.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height)) + var textFrame = CGRect(origin: CGPoint(x: 0.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height)) + if case .inlineNavigation = self.fieldStyle { + textFrame.size.width = boundingSize.width - 27.0 + textBackgroundFrame.size.width = boundingSize.width + } else { + textFrame.origin.x = textBackgroundFrame.minX + 24.0 + } if let iconImage = self.iconNode.image { let iconSize = iconImage.size diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index 50420e6b50..3c885e33e0 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -25,12 +25,14 @@ public final class SearchDisplayController { } } - private let searchBar: SearchBarNode + private var searchBar: SearchBarNode? + private let searchBarIsExternal: Bool private let mode: SearchDisplayControllerMode private let backgroundNode: BackgroundNode public let contentNode: SearchDisplayControllerContentNode private var hasSeparator: Bool private let inline: Bool + private let cancel: () -> Void private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -38,71 +40,75 @@ public final class SearchDisplayController { private var isSearchingDisposable: Disposable? - public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, hasBackground: Bool = false, hasSeparator: Bool = false, contentNode: SearchDisplayControllerContentNode, inline: Bool = false, cancel: @escaping () -> Void) { + public init( + presentationData: PresentationData, + mode: SearchDisplayControllerMode = .navigation, + placeholder: String? = nil, + hasBackground: Bool = false, + hasSeparator: Bool = false, + contentNode: SearchDisplayControllerContentNode, + inline: Bool = false, + cancel: @escaping () -> Void, + fieldStyle: SearchBarStyle = .modern, + searchBarIsExternal: Bool = false + ) { self.inline = inline - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator, inline: inline), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator, displayBackground: hasBackground) + self.cancel = cancel + self.searchBarIsExternal = searchBarIsExternal + + if !searchBarIsExternal { + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator, inline: inline), strings: presentationData.strings, fieldStyle: fieldStyle, forceSeparator: hasSeparator, displayBackground: hasBackground) + } self.backgroundNode = BackgroundNode() self.backgroundNode.allowsGroupOpacity = true self.mode = mode self.contentNode = contentNode self.hasSeparator = hasSeparator + + if let searchBar = self.searchBar { + self.setSearchBar(searchBar) + } - self.searchBar.textUpdated = { [weak contentNode] text, _ in - contentNode?.searchTextUpdated(text: text) - } - self.searchBar.tokensUpdated = { [weak contentNode] tokens in - contentNode?.searchTokensUpdated(tokens: tokens) - } - self.searchBar.cancel = { [weak self] in - self?.isDeactivating = true - cancel() - } - self.searchBar.clearPrefix = { [weak contentNode] in - contentNode?.searchTextClearPrefix() - } - self.searchBar.clearTokens = { [weak contentNode] in - contentNode?.searchTextClearTokens() - } self.contentNode.cancel = { [weak self] in self?.isDeactivating = true cancel() } self.contentNode.dismissInput = { [weak self] in - self?.searchBar.deactivate(clear: false) + self?.searchBar?.deactivate(clear: false) } var isFirstTime = true self.contentNode.setQuery = { [weak self] prefix, tokens, query in - if let strongSelf = self { - strongSelf.searchBar.prefixString = prefix - let previousTokens = strongSelf.searchBar.tokens - strongSelf.searchBar.tokens = tokens - strongSelf.searchBar.text = query + if let strongSelf = self, let searchBar = strongSelf.searchBar { + searchBar.prefixString = prefix + let previousTokens = searchBar.tokens + searchBar.tokens = tokens + searchBar.text = query if previousTokens.count < tokens.count && !isFirstTime { if let lastToken = tokens.last, !lastToken.permanent { - strongSelf.searchBar.selectLastToken() + searchBar.selectLastToken() } } isFirstTime = false } } if let placeholder = placeholder { - self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + self.searchBar?.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) } self.contentNode.setPlaceholder = { [weak self] string in - guard string != self?.searchBar.placeholderString?.string else { + guard string != self?.searchBar?.placeholderString?.string else { return } - if let mutableAttributedString = self?.searchBar.placeholderString?.mutableCopy() as? NSMutableAttributedString { + if let mutableAttributedString = self?.searchBar?.placeholderString?.mutableCopy() as? NSMutableAttributedString { mutableAttributedString.mutableString.setString(string) - self?.searchBar.placeholderString = mutableAttributedString + self?.searchBar?.placeholderString = mutableAttributedString } } self.isSearchingDisposable = (contentNode.isSearching |> deliverOnMainQueue).start(next: { [weak self] value in - self?.searchBar.activity = value + self?.searchBar?.activity = value }) if self.contentNode.hasDim { @@ -113,9 +119,32 @@ public final class SearchDisplayController { self.backgroundNode.isTransparent = false } } + + public func setSearchBar(_ searchBar: SearchBarNode) { + self.searchBar = searchBar + + searchBar.textUpdated = { [weak contentNode] text, _ in + contentNode?.searchTextUpdated(text: text) + } + searchBar.tokensUpdated = { [weak contentNode] tokens in + contentNode?.searchTokensUpdated(tokens: tokens) + } + searchBar.cancel = { [weak self] in + self?.isDeactivating = true + self?.cancel() + } + searchBar.clearPrefix = { [weak contentNode] in + contentNode?.searchTextClearPrefix() + } + searchBar.clearTokens = { [weak contentNode] in + contentNode?.searchTextClearTokens() + } + } public func updatePresentationData(_ presentationData: PresentationData) { - self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator, inline: self.inline), strings: presentationData.strings) + if !self.searchBarIsExternal { + self.searchBar?.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator, inline: self.inline), strings: presentationData.strings) + } self.contentNode.updatePresentationData(presentationData) if self.contentNode.hasDim { @@ -137,21 +166,27 @@ public final class SearchDisplayController { navigationBarOffset = 0.0 } var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight)) + if self.searchBarIsExternal { + navigationBarFrame.size.height -= 10.0 + } if layout.statusBarHeight == nil { navigationBarFrame.size.height = 64.0 } navigationBarFrame.size.height += 10.0 + let navigationBarHeight = navigationBarFrame.maxY - let searchBarFrame: CGRect - if case .navigation = self.mode { - searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) - } else { - searchBarFrame = navigationBarFrame + if !self.searchBarIsExternal, let searchBar = self.searchBar { + let searchBarFrame: CGRect + if case .navigation = self.mode { + searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) + } else { + searchBarFrame = navigationBarFrame + } + transition.updateFrame(node: searchBar, frame: searchBarFrame) + searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) } - transition.updateFrame(node: self.searchBar, frame: searchBarFrame) - self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) - self.containerLayout = (layout, navigationBarFrame.maxY) + self.containerLayout = (layout, navigationBarHeight) let bounds = CGRect(origin: CGPoint(), size: layout.size) transition.updateFrame(node: self.backgroundNode, frame: bounds.insetBy(dx: -20.0, dy: -20.0)) @@ -205,18 +240,20 @@ public final class SearchDisplayController { self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } - if !self.contentNode.hasDim { - if let placeholder = placeholder { - self.searchBar.placeholderString = placeholder.placeholderString - } - } else { - if let placeholder = placeholder { - let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) - let contentNodePosition = self.backgroundNode.layer.position - if contentNode.animateBackgroundAppearance { - self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if !self.searchBarIsExternal { + if !self.contentNode.hasDim { + if let placeholder = placeholder { + self.searchBar?.placeholderString = placeholder.placeholderString + } + } else { + if let placeholder = placeholder { + let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) + let contentNodePosition = self.backgroundNode.layer.position + if contentNode.animateBackgroundAppearance { + self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + self.searchBar?.placeholderString = placeholder.placeholderString } - self.searchBar.placeholderString = placeholder.placeholderString } } @@ -240,39 +277,49 @@ public final class SearchDisplayController { navigationBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) } - self.searchBar.frame = navigationBarFrame - insertSubnode(self.searchBar, true) - self.searchBar.layout() - - if focus { - self.searchBar.activate() + if !self.searchBarIsExternal, let searchBar = self.searchBar { + searchBar.frame = navigationBarFrame + insertSubnode(searchBar, true) + searchBar.layout() + + if focus { + searchBar.activate() + } } + if let placeholder = placeholder { - self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if !self.searchBarIsExternal, let searchBar = self.searchBar { + searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } if self.contentNode.hasDim { self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } else { - self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + if !self.searchBarIsExternal, let searchBar = self.searchBar { + searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + } self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) { - self.searchBar.deactivate(clear: false) + if let searchBar = self.searchBar { + searchBar.deactivate(clear: false) + } - let searchBar = self.searchBar - if let placeholder = placeholder { - searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { - [weak searchBar] in - searchBar?.removeFromSupernode() - }) - } else { - searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] finished in - if finished { + if !self.searchBarIsExternal, let searchBar = self.searchBar { + if let placeholder = placeholder { + searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { + [weak searchBar] in searchBar?.removeFromSupernode() - } - }) + }) + } else { + searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] finished in + if finished { + searchBar?.removeFromSupernode() + } + }) + } } let backgroundNode = self.backgroundNode diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index abee862105..c346769805 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -18,15 +18,18 @@ final class TabBarControllerNode: ASDisplayNode { let layout: ContainerViewLayout let toolbar: Toolbar? let isTabBarHidden: Bool + let currentControllerSearchState: ViewController.TabBarSearchState? init( layout: ContainerViewLayout, toolbar: Toolbar?, - isTabBarHidden: Bool + isTabBarHidden: Bool, + currentControllerSearchState: ViewController.TabBarSearchState? ) { self.layout = layout self.toolbar = toolbar self.isTabBarHidden = isTabBarHidden + self.currentControllerSearchState = currentControllerSearchState } } @@ -51,6 +54,7 @@ final class TabBarControllerNode: ASDisplayNode { } private var theme: PresentationTheme + private var strings: PresentationStrings private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void private let contextAction: (Int, ContextExtractedContentContainingView, ContextGesture) -> Void @@ -60,24 +64,26 @@ final class TabBarControllerNode: ASDisplayNode { private var toolbarNode: ToolbarNode? private let toolbarActionSelected: (ToolbarActionOption) -> Void private let disabledPressed: () -> Void + private let activateSearch: () -> Void + private let deactivateSearch: () -> Void private(set) var tabBarItems: [TabBarNodeItem] = [] private(set) var selectedIndex: Int = 0 - private(set) var currentControllerNode: ASDisplayNode? + private weak var currentController: ViewController? private var layoutResult: LayoutResult? private var isUpdateRequested: Bool = false private var isChangingSelectedIndex: Bool = false - func setCurrentControllerNode(_ node: ASDisplayNode?) -> () -> Void { - guard node !== self.currentControllerNode else { + func setCurrentController(_ controller: ViewController?) -> () -> Void { + guard controller !== self.currentController else { return {} } - let previousNode = self.currentControllerNode - self.currentControllerNode = node - if let currentControllerNode = self.currentControllerNode { + let previousNode = self.currentController?.displayNode + self.currentController = controller + if let currentControllerNode = self.currentController?.displayNode { if let previousNode { self.insertSubnode(currentControllerNode, aboveSubnode: previousNode) } else { @@ -89,14 +95,22 @@ final class TabBarControllerNode: ASDisplayNode { } return { [weak self, weak previousNode] in - if previousNode !== self?.currentControllerNode { + if previousNode !== self?.currentController?.displayNode { previousNode?.removeFromSupernode() } } } + + var currentSearchNode: ASDisplayNode? { + if let tabBarComponentView = self.tabBarView.view as? TabBarComponent.View { + return tabBarComponentView.currentSearchNode + } + return nil + } - init(theme: PresentationTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void, activateSearch: @escaping () -> Void, deactivateSearch: @escaping () -> Void) { self.theme = theme + self.strings = strings self.itemSelected = itemSelected self.contextAction = contextAction self.disabledOverlayNode = ASDisplayNode() @@ -104,7 +118,9 @@ final class TabBarControllerNode: ASDisplayNode { self.disabledOverlayNode.alpha = 0.0 self.toolbarActionSelected = toolbarActionSelected self.disabledPressed = disabledPressed - + self.activateSearch = activateSearch + self.deactivateSearch = deactivateSearch + super.init() self.setViewBlock({ @@ -163,7 +179,7 @@ final class TabBarControllerNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) -> CGFloat { - let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden) + let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden, currentControllerSearchState: self.currentController?.tabBarSearchState) if let layoutResult = self.layoutResult, layoutResult.params == params { return layoutResult.bottomInset } else { @@ -179,18 +195,27 @@ final class TabBarControllerNode: ASDisplayNode { } private func updateImpl(params: Params, transition: ContainedViewLayoutTransition) -> CGFloat { - var options: ContainerViewLayoutInsetOptions = [] - if params.layout.metrics.widthClass == .regular { - options.insert(.input) + var panelsBottomInset: CGFloat = params.layout.insets(options: []).bottom + if params.layout.metrics.widthClass == .regular, let inputHeight = params.layout.inputHeight, inputHeight != 0.0 { + panelsBottomInset = inputHeight + 8.0 } - - var bottomInset: CGFloat = params.layout.insets(options: options).bottom - if bottomInset == 0.0 { - bottomInset = 8.0 + if panelsBottomInset == 0.0 { + panelsBottomInset = 8.0 } else { - bottomInset = max(bottomInset, 8.0) + panelsBottomInset = max(panelsBottomInset, 8.0) + } + + var tabBarBottomInset: CGFloat = panelsBottomInset + if let currentController = self.currentController { + if let tabBarSearchState = currentController.tabBarSearchState, tabBarSearchState.isActive, let inputHeight = params.layout.inputHeight, inputHeight != 0.0 { + tabBarBottomInset = max(tabBarBottomInset, inputHeight + 8.0) + } + } + + var sideInset: CGFloat = 12.0 + if tabBarBottomInset <= 28.0 { + sideInset = 20.0 } - let sideInset: CGFloat = 20.0 var selectedId: AnyHashable? if self.selectedIndex < self.tabBarItems.count { @@ -208,6 +233,7 @@ final class TabBarControllerNode: ASDisplayNode { transition: tabBarTransition, component: AnyComponent(TabBarComponent( theme: self.theme, + strings: self.strings, items: self.tabBarItems.map { item in let itemId = AnyHashable(ObjectIdentifier(item.item)) return TabBarComponent.Item( @@ -230,13 +256,30 @@ final class TabBarControllerNode: ASDisplayNode { } ) }, + search: self.currentController?.tabBarSearchState.flatMap { tabBarSearchState in + return TabBarComponent.Search( + isActive: tabBarSearchState.isActive, + activate: { [weak self] in + guard let self else { + return + } + self.activateSearch() + }, + deactivate: { [weak self] in + guard let self else { + return + } + self.deactivateSearch() + } + ) + }, selectedId: selectedId, isTablet: params.layout.metrics.isTablet )), environment: {}, containerSize: CGSize(width: params.layout.size.width - sideInset * 2.0, height: 100.0) ) - let tabBarFrame = CGRect(origin: CGPoint(x: floor((params.layout.size.width - tabBarSize.width) * 0.5), y: params.layout.size.height - (self.tabBarHidden ? 0.0 : (tabBarSize.height + bottomInset))), size: tabBarSize) + let tabBarFrame = CGRect(origin: CGPoint(x: floor((params.layout.size.width - tabBarSize.width) * 0.5), y: params.layout.size.height - (self.tabBarHidden ? 0.0 : (tabBarSize.height + tabBarBottomInset))), size: tabBarSize) if let tabBarComponentView = self.tabBarView.view { if tabBarComponentView.superview == nil { @@ -248,13 +291,13 @@ final class TabBarControllerNode: ASDisplayNode { transition.updateFrame(node: self.disabledOverlayNode, frame: tabBarFrame) - let toolbarHeight = 50.0 + params.layout.insets(options: options).bottom + let toolbarHeight = 50.0 + panelsBottomInset let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: params.layout.size.height - toolbarHeight), size: CGSize(width: params.layout.size.width, height: toolbarHeight)) if let toolbar = params.toolbar { if let toolbarNode = self.toolbarNode { transition.updateFrame(node: toolbarNode, frame: toolbarFrame) - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition) + toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: panelsBottomInset, toolbar: toolbar, transition: transition) } else { let toolbarNode = ToolbarNode(theme: ToolbarTheme(theme: self.theme), displaySeparator: true, left: { [weak self] in self?.toolbarActionSelected(.left) @@ -264,7 +307,7 @@ final class TabBarControllerNode: ASDisplayNode { self?.toolbarActionSelected(.middle) }) toolbarNode.frame = toolbarFrame - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate) + toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: panelsBottomInset, toolbar: toolbar, transition: .immediate) self.addSubnode(toolbarNode) self.toolbarNode = toolbarNode if transition.isAnimated { diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index a3f7fe0806..111fe208d1 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -93,9 +93,11 @@ open class TabBarControllerImpl: ViewController, TabBarController { private let pendingControllerDisposable = MetaDisposable() private var theme: PresentationTheme + private var strings: PresentationStrings - public init(theme: PresentationTheme) { + public init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme + self.strings = strings super.init(navigationBarPresentationData: nil) @@ -152,7 +154,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { } override open func loadDisplayNode() { - self.displayNode = TabBarControllerNode(theme: self.theme, itemSelected: { [weak self] index, longTap, itemNodes in + self.displayNode = TabBarControllerNode(theme: self.theme, strings: self.strings, itemSelected: { [weak self] index, longTap, itemNodes in if let strongSelf = self { if longTap, let controller = strongSelf.controllers[index] as? TabBarContainedController { controller.presentTabBarPreviewingController(sourceNodes: itemNodes) @@ -235,6 +237,16 @@ open class TabBarControllerImpl: ViewController, TabBarController { self?.currentController?.toolbarActionSelected(action: action) }, disabledPressed: { [weak self] in self?.currentController?.tabBarDisabledAction() + }, activateSearch: { [weak self] in + guard let self else { + return + } + self.currentController?.tabBarActivateSearch() + }, deactivateSearch: { [weak self] in + guard let self else { + return + } + self.currentController?.tabBarDeactivateSearch() }) self.updateSelectedIndex() @@ -263,7 +275,8 @@ open class TabBarControllerImpl: ViewController, TabBarController { } if let currentController = self.currentController { currentController.willMove(toParent: nil) - //self.tabBarControllerNode.currentControllerNode = nil + currentController.tabBarSearchStateUpdated = nil + currentController.currentTabBarSearchNode = nil if animated { currentController.view.layer.animateScale(from: 1.0, to: transitionScale, duration: 0.12, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { completed in @@ -286,7 +299,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { currentController.willMove(toParent: self) self.addChild(currentController) - let commit = self.tabBarControllerNode.setCurrentControllerNode(currentController.displayNode) + let commit = self.tabBarControllerNode.setCurrentController(currentController) if animated { currentController.view.layer.animateScale(from: transitionScale, to: 1.0, duration: 0.15, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring) currentController.view.layer.allowsGroupOpacity = true @@ -303,6 +316,22 @@ open class TabBarControllerImpl: ViewController, TabBarController { currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle + + currentController.tabBarSearchStateUpdated = { [weak self] transition in + guard let self else { + return + } + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: transition) + } + } + + currentController.currentTabBarSearchNode = { [weak self] in + guard let self else { + return nil + } + return self.tabBarControllerNode.currentSearchNode + } } if let layout = self.validLayout { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 903663967c..99ef8a56b5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -1174,6 +1174,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.touchDownGestureRecognizer = recognizer textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string + + self.isAccessibilityContainer = true + self.accessibilityElements = [textInputNode.textView] } private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics, bottomInset: CGFloat) -> CGFloat { diff --git a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD new file mode 100644 index 0000000000..4805b62a3d --- /dev/null +++ b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatListFilterTabContainerNode", + module_name = "ChatListFilterTabContainerNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/LiquidLens", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift similarity index 96% rename from submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift rename to submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift index b20fc85916..9e3fb4dd03 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift @@ -210,7 +210,7 @@ private final class ItemNode: ASDisplayNode { if self.theme !== presentationData.theme { self.theme = presentationData.theme - self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeActiveBackgroundColor) + self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chat.inputPanel.panelControlColor) self.badgeBackgroundInactiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor) themeUpdated = true @@ -299,10 +299,10 @@ private final class ItemNode: ASDisplayNode { if themeUpdated || titleUpdated { self.titleNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.chat.inputPanel.panelControlColor) self.shortTitleNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.chat.inputPanel.panelControlColor) } if unreadCount != 0 { @@ -438,8 +438,8 @@ public enum ChatListFilterTabEntryId: Hashable { } public struct ChatListFilterTabEntryUnreadCount: Equatable { - let value: Int - let hasUnmuted: Bool + public let value: Int + public let hasUnmuted: Bool public init(value: Int, hasUnmuted: Bool) { self.value = value @@ -482,14 +482,14 @@ public enum ChatListFilterTabEntry: Equatable { public final class ChatListFilterTabContainerNode: ASDisplayNode { private let context: AccountContext private let scrollNode: ASScrollNode - private let selectedLineNode: ASImageNode + private let selectedBackgroundNode: ASImageNode private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:] public var tabSelected: ((ChatListFilterTabEntryId, Bool) -> Void)? - var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? - var addFilter: (() -> Void)? - var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? - var presentPremiumTip: (() -> Void)? + public var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? + public var addFilter: (() -> Void)? + public var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? + public var presentPremiumTip: (() -> Void)? private var reorderingGesture: ReorderingGestureRecognizer? private var reorderingItem: ChatListFilterTabEntryId? @@ -501,7 +501,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, canReorderAllChats: Bool, filtersLimit: Int32?, transitionFraction: CGFloat, presentationData: PresentationData)? - var reorderedFilterIds: [Int32]? { + public var reorderedFilterIds: [Int32]? { return self.reorderedItemIds.flatMap { $0.compactMap { switch $0 { @@ -533,9 +533,9 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.context = context self.scrollNode = ASScrollNode() - self.selectedLineNode = ASImageNode() - self.selectedLineNode.displaysAsynchronously = false - self.selectedLineNode.displayWithoutProcessing = true + self.selectedBackgroundNode = ASImageNode() + self.selectedBackgroundNode.displaysAsynchronously = false + self.selectedBackgroundNode.displayWithoutProcessing = true super.init() @@ -549,7 +549,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } self.addSubnode(self.scrollNode) - self.scrollNode.addSubnode(self.selectedLineNode) + self.scrollNode.addSubnode(self.selectedBackgroundNode) let reorderingGesture = ReorderingGestureRecognizer(shouldBegin: { [weak self] point in guard let strongSelf = self else { @@ -677,7 +677,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { private var previousSelectedFrame: CGRect? public func cancelAnimations() { - self.selectedLineNode.layer.removeAllAnimations() + self.selectedBackgroundNode.layer.removeAllAnimations() self.scrollNode.layer.removeAllAnimations() } @@ -695,14 +695,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { let previousContentWidth = self.scrollNode.view.contentSize.width if self.currentParams?.presentationData.theme !== presentationData.theme { - self.selectedLineNode.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 4.0, height: 4.0))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: 0.0), size: CGSize(width: 4.0, height: 4.0))) - context.fill(CGRect(x: 2.0, y: 0.0, width: size.width - 4.0, height: 4.0)) - context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0)) - })?.resizableImage(withCapInsets: UIEdgeInsets(top: 3.0, left: 3.0, bottom: 0.0, right: 3.0), resizingMode: .stretch) + self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: presentationData.theme.chatList.itemHighlightedBackgroundColor) } if isReordering { @@ -864,7 +857,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { let minSpacing: CGFloat = 26.0 - let resolvedSideInset: CGFloat = 16.0 + sideInset + let resolvedSideInset: CGFloat = 25.0 + sideInset var leftOffset: CGFloat = resolvedSideInset var longTitlesWidth: CGFloat = resolvedSideInset @@ -937,13 +930,13 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } if let selectedFrame = selectedFrame { - let wasAdded = self.selectedLineNode.isHidden - self.selectedLineNode.isHidden = false - let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 3.0), size: CGSize(width: selectedFrame.width, height: 3.0)) + let wasAdded = self.selectedBackgroundNode.isHidden + self.selectedBackgroundNode.isHidden = false + let selectedBackgroundFrame = CGRect(origin: CGPoint(x: selectedFrame.minX - 10.0, y: selectedFrame.minY - floor((36.0 - selectedFrame.height) * 0.5)), size: CGSize(width: selectedFrame.width + 10.0 * 2.0, height: 36.0)) if wasAdded { - self.selectedLineNode.frame = lineFrame + self.selectedBackgroundNode.frame = selectedBackgroundFrame } else { - transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) + transition.updateFrame(node: self.selectedBackgroundNode, frame: selectedBackgroundFrame) } if let previousSelectedFrame = self.previousSelectedFrame { @@ -972,7 +965,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) self.previousSelectedFrame = selectedFrame } else { - self.selectedLineNode.isHidden = true + self.selectedBackgroundNode.isHidden = true self.previousSelectedAbsFrame = nil self.previousSelectedFrame = nil } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD index 6f94cb0c07..5355cc429f 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD @@ -24,6 +24,8 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters", "//submodules/SearchUI", "//submodules/TelegramUI/Components/MoreHeaderButton", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 8e235da5ea..571c5cfe77 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -9,6 +9,7 @@ import AppBundle import StoryPeerListComponent import TelegramCore import MoreHeaderButton +import GlassBackgroundComponent public final class HeaderNetworkStatusComponent: Component { public enum Content: Equatable { @@ -79,7 +80,6 @@ public final class ChatListHeaderComponent: Component { public let chatListTitle: NetworkStatusTitle? public let leftButton: AnyComponentWithIdentity? public let rightButtons: [AnyComponentWithIdentity] - public let backTitle: String? public let backPressed: (() -> Void)? public init( @@ -89,7 +89,6 @@ public final class ChatListHeaderComponent: Component { chatListTitle: NetworkStatusTitle?, leftButton: AnyComponentWithIdentity?, rightButtons: [AnyComponentWithIdentity], - backTitle: String?, backPressed: (() -> Void)? ) { self.title = title @@ -98,7 +97,6 @@ public final class ChatListHeaderComponent: Component { self.chatListTitle = chatListTitle self.leftButton = leftButton self.rightButtons = rightButtons - self.backTitle = backTitle self.backPressed = backPressed } @@ -121,7 +119,7 @@ public final class ChatListHeaderComponent: Component { if lhs.rightButtons != rhs.rightButtons { return false } - if lhs.backTitle != rhs.backTitle { + if (lhs.backPressed == nil) != (rhs.backPressed == nil) { return false } return true @@ -226,8 +224,6 @@ public final class ChatListHeaderComponent: Component { private let onPressed: () -> Void let arrowView: UIImageView - let titleOffsetContainer: UIView - let titleView: ImmediateTextView private var currentColor: UIColor? @@ -235,16 +231,11 @@ public final class ChatListHeaderComponent: Component { self.onPressed = onPressed self.arrowView = UIImageView() - self.titleOffsetContainer = UIView() - self.titleView = ImmediateTextView() super.init(frame: CGRect()) self.addSubview(self.arrowView) - self.addSubview(self.titleOffsetContainer) - self.titleOffsetContainer.addSubview(self.titleView) - self.highligthedChanged = { [weak self] highlighted in guard let self else { return @@ -266,37 +257,37 @@ public final class ChatListHeaderComponent: Component { self.onPressed() } - func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) - let titleTextUpdated = self.titleView.attributedText != titleText - self.titleView.attributedText = titleText - let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) - - self.accessibilityLabel = title + func update(theme: PresentationTheme, strings: PresentationStrings, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.accessibilityLabel = strings.Common_Back self.accessibilityTraits = [.button] - if self.currentColor != theme.rootController.navigationBar.accentTextColor { - self.currentColor = theme.rootController.navigationBar.accentTextColor - self.arrowView.image = NavigationBarTheme.generateBackArrowImage(color: theme.rootController.navigationBar.accentTextColor) + if self.currentColor != theme.chat.inputPanel.panelControlColor { + self.currentColor = theme.chat.inputPanel.panelControlColor + let imageSize = CGSize(width: 44.0, height: 44.0) + let topRightPoint = CGPoint(x: 24.6, y: 14.0) + let centerPoint = CGPoint(x: 17.0, y: imageSize.height * 0.5) + self.arrowView.image = generateImage(imageSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setLineJoin(.round) + context.move(to: topRightPoint) + context.addLine(to: centerPoint) + context.addLine(to: CGPoint(x: topRightPoint.x, y: size.height - topRightPoint.y)) + context.strokePath() + })?.withRenderingMode(.alwaysTemplate) + self.arrowView.tintColor = theme.chat.inputPanel.panelControlColor } - let iconSpacing: CGFloat = 8.0 - let iconOffset: CGFloat = -7.0 - + let size = CGSize(width: 44.0, height: availableSize.height) let arrowSize = self.arrowView.image?.size ?? CGSize(width: 13.0, height: 22.0) - let arrowFrame = CGRect(origin: CGPoint(x: iconOffset - 1.0, y: floor((availableSize.height - arrowSize.height) / 2.0)), size: arrowSize) + let arrowFrame = arrowSize.centered(in: CGRect(origin: CGPoint(), size: size)) transition.setPosition(view: self.arrowView, position: arrowFrame.center) transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) - let titleFrame = CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) - if titleTextUpdated { - self.titleView.frame = titleFrame - } else { - transition.setFrame(view: self.titleView, frame: titleFrame) - } - - return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) + return size } } @@ -305,9 +296,9 @@ public final class ChatListHeaderComponent: Component { let openStatusSetup: (UIView) -> Void let toggleIsLocked: () -> Void - let leftButtonOffsetContainer: UIView + let leftButtonsContainer: UIView var leftButtonViews: [AnyHashable: ComponentView] = [:] - let rightButtonOffsetContainer: UIView + let rightButtonsContainer: UIView var rightButtonViews: [AnyHashable: ComponentView] = [:] var backButtonView: BackButtonView? @@ -324,6 +315,9 @@ public final class ChatListHeaderComponent: Component { private(set) var centerContentOffsetX: CGFloat = 0.0 private(set) var centerContentOrigin: CGFloat = 0.0 + + private(set) var leftButtonsWidth: CGFloat = 0.0 + private(set) var rightButtonsWidth: CGFloat = 0.0 init( backPressed: @escaping () -> Void, @@ -334,8 +328,9 @@ public final class ChatListHeaderComponent: Component { self.openStatusSetup = openStatusSetup self.toggleIsLocked = toggleIsLocked - self.leftButtonOffsetContainer = UIView() - self.rightButtonOffsetContainer = UIView() + self.leftButtonsContainer = UIView() + self.rightButtonsContainer = UIView() + self.titleOffsetContainer = UIView() self.titleScaleContainer = UIView() @@ -345,8 +340,6 @@ public final class ChatListHeaderComponent: Component { self.addSubview(self.titleOffsetContainer) self.titleOffsetContainer.addSubview(self.titleScaleContainer) - self.addSubview(self.leftButtonOffsetContainer) - self.addSubview(self.rightButtonOffsetContainer) self.titleScaleContainer.addSubview(self.titleTextView) } @@ -393,12 +386,12 @@ public final class ChatListHeaderComponent: Component { transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform) } - func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in - completion() - }) - transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + func updateNavigationTransitionAsPrevious(nextView: ContentView, width: CGFloat, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: fraction * 10.0) + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: fraction * 10.0) if let backButtonView = self.backButtonView { transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: backButtonView.bounds.size), completion: { _ in @@ -406,80 +399,53 @@ public final class ChatListHeaderComponent: Component { }) } - if let chatListTitleView = self.chatListTitleView, let nextBackButtonView = nextView.backButtonView { - let titleFrame = chatListTitleView.titleNode.view.convert(chatListTitleView.titleNode.bounds, to: self.titleOffsetContainer) - let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: nextView) - - let totalOffset = titleFrame.minX - backButtonTitleFrame.minX - - transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) - } + let totalOffset = -width * fraction + + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) } func updateNavigationTransitionAsNext(previousView: ContentView, storyPeerListView: StoryPeerListComponent.View?, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) - transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) - if let backButtonView = self.backButtonView { - transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) - transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) - - if let storyPeerListView { - let previousTitleFrame = storyPeerListView.titleFrame() - let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self) - - let totalOffset = previousTitleFrame.minX - backButtonTitleFrame.minX - - transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) - } else if let previousChatListTitleView = previousView.chatListTitleView { - let previousTitleFrame = previousChatListTitleView.titleNode.view.convert(previousChatListTitleView.titleNode.bounds, to: previousView.titleOffsetContainer) - let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self) - - let totalOffset = previousTitleFrame.minX - backButtonTitleFrame.minX - - transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) - } - } + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) + + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) } func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in - }) - transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + + alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: fraction * 10.0) + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in completion() }) - - if let backButtonView = self.backButtonView { - transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.bounds.size), completion: { _ in - }) - } + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: fraction * 10.0) transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size)) transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) } func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) - transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) - if let backButtonView = self.backButtonView { - transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) - transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) - - transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) - } + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) + alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(fraction, 2.0)) + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) } - func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { + func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, displayBackButton: Bool, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size)) @@ -490,11 +456,10 @@ public final class ChatListHeaderComponent: Component { let titleTextUpdated = self.titleTextView.attributedText != titleText self.titleTextView.attributedText = titleText - let buttonSpacing: CGFloat = 8.0 + let buttonSpacing: CGFloat = 0.0 + var nextLeftButtonX: CGFloat = 0.0 - var leftOffset = sideInset - - if let backTitle = backTitle { + if displayBackButton { var backButtonTransition = transition let backButtonView: BackButtonView if let current = self.backButtonView { @@ -508,11 +473,14 @@ public final class ChatListHeaderComponent: Component { self.backPressed() }) self.backButtonView = backButtonView - self.addSubview(backButtonView) + self.leftButtonsContainer.addSubview(backButtonView) } - let backButtonSize = backButtonView.update(title: backTitle, theme: theme, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition) - backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize)) - leftOffset += backButtonSize.width + buttonSpacing + let backButtonSize = backButtonView.update(theme: theme, strings: strings, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition) + backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: nextLeftButtonX, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize)) + if nextLeftButtonX != 0.0 { + nextLeftButtonX += buttonSpacing + } + nextLeftButtonX += backButtonSize.width } else if let backButtonView = self.backButtonView { self.backButtonView = nil backButtonView.removeFromSuperview() @@ -521,6 +489,10 @@ public final class ChatListHeaderComponent: Component { var validLeftButtons = Set() if let leftButton = content.leftButton { validLeftButtons.insert(leftButton.id) + + if nextLeftButtonX != 0.0 { + nextLeftButtonX += buttonSpacing + } var buttonTransition = transition var animateButtonIn = false @@ -541,23 +513,25 @@ public final class ChatListHeaderComponent: Component { }, containerSize: CGSize(width: 100.0, height: size.height) ) - let buttonFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: nextLeftButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { - self.leftButtonOffsetContainer.addSubview(buttonComponentView) + self.leftButtonsContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { - transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) + alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } - leftOffset = buttonFrame.maxX + buttonSpacing + nextLeftButtonX += buttonSize.width } var removeLeftButtons: [AnyHashable] = [] for (id, buttonView) in self.leftButtonViews { if !validLeftButtons.contains(id) { if let buttonComponentView = buttonView.view { - transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) + alphaTransition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in buttonComponentView?.removeFromSuperview() }) } @@ -568,10 +542,14 @@ public final class ChatListHeaderComponent: Component { self.leftButtonViews.removeValue(forKey: id) } - var rightOffset = size.width - sideInset + var nextRightButtonX: CGFloat = 0.0 var validRightButtons = Set() - for rightButton in content.rightButtons { + for rightButton in content.rightButtons.reversed() { validRightButtons.insert(rightButton.id) + + if nextRightButtonX != 0.0 { + nextRightButtonX += buttonSpacing + } var buttonTransition = transition var animateButtonIn = false @@ -592,23 +570,25 @@ public final class ChatListHeaderComponent: Component { }, containerSize: CGSize(width: 100.0, height: size.height) ) - let buttonFrame = CGRect(origin: CGPoint(x: rightOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: nextRightButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { - self.rightButtonOffsetContainer.addSubview(buttonComponentView) + self.rightButtonsContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { - transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) + alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } - rightOffset = buttonFrame.minX - buttonSpacing + nextRightButtonX += buttonSize.width } var removeRightButtons: [AnyHashable] = [] for (id, buttonView) in self.rightButtonViews { if !validRightButtons.contains(id) { if let buttonComponentView = buttonView.view { - transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) + alphaTransition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in buttonComponentView?.removeFromSuperview() }) } @@ -618,8 +598,11 @@ public final class ChatListHeaderComponent: Component { for id in removeRightButtons { self.rightButtonViews.removeValue(forKey: id) } - - let commonInset: CGFloat = max(leftOffset, size.width - rightOffset) + + self.leftButtonsWidth = nextLeftButtonX + self.rightButtonsWidth = nextRightButtonX + + let commonInset: CGFloat = sideInset + max(nextLeftButtonX, nextRightButtonX) + 8.0 let remainingWidth = size.width - commonInset * 2.0 let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) @@ -662,10 +645,10 @@ public final class ChatListHeaderComponent: Component { } var centerContentLeftInset: CGFloat = 0.0 - centerContentLeftInset = leftOffset - 4.0 + centerContentLeftInset = nextLeftButtonX + 4.0 var centerContentRightInset: CGFloat = 0.0 - centerContentRightInset = size.width - rightOffset - 8.0 + centerContentRightInset = nextRightButtonX + 20.0 var centerContentWidth: CGFloat = 0.0 var centerContentOffsetX: CGFloat = 0.0 @@ -690,10 +673,8 @@ public final class ChatListHeaderComponent: Component { let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition) centerContentWidth = floor((chatListTitleContentSize.width * 0.5 - titleContentRect.minX) * 2.0) - //sideWidth + centerWidth + centerOffset = size.width - //let centerOffset = -(size.width - (sideContentWidth + centerContentWidth)) * 0.5 + size.width * 0.5 let centerOffset = sideContentWidth * 0.5 - centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - rightOffset) + centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - (size.width - sideInset - nextRightButtonX)) chatListTitleView.openStatusSetup = { [weak self] sourceView in guard let self else { @@ -741,6 +722,11 @@ public final class ChatListHeaderComponent: Component { private var primaryContentView: ContentView? private var secondaryContentView: ContentView? private var storyOffsetFraction: CGFloat = 0.0 + + private let leftButtonsContainer: UIView + private let rightButtonsContainer: UIView + private var leftButtonsBackgroundContainer: GlassBackgroundView? + private var rightButtonsBackgroundContainer: GlassBackgroundView? private let storyPeerListExternalState = StoryPeerListComponent.ExternalState() private var storyPeerList: ComponentView? @@ -753,6 +739,10 @@ public final class ChatListHeaderComponent: Component { } override init(frame: CGRect) { + self.leftButtonsContainer = UIView() + self.rightButtonsContainer = UIView() + self.rightButtonsContainer.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + super.init(frame: frame) self.storyOffsetFraction = 1.0 @@ -766,10 +756,6 @@ public final class ChatListHeaderComponent: Component { return self.effectiveContentView?.backButtonView?.arrowView } - public var backButtonTitleView: UIView? { - return self.effectiveContentView?.backButtonView?.titleView - } - public var rightButtonView: UIView? { return self.effectiveContentView?.rightButtonViews.first?.value.view } @@ -795,18 +781,6 @@ public final class ChatListHeaderComponent: Component { } } - public func makeTransitionBackButtonView(accentColor: UIColor) -> UIView? { - if let backButtonTitleView = self.backButtonTitleView as? ImmediateTextView { - let view = ImmediateTextView() - view.attributedText = NSAttributedString(string: backButtonTitleView.attributedText?.string ?? "", font: Font.regular(17.0), textColor: accentColor) - let _ = view.updateLayout(CGSize(width: 100.0, height: 100.0)) - view.frame = backButtonTitleView.convert(backButtonTitleView.bounds, to: self) - return view - } else { - return nil - } - } - public func storyPeerListView() -> StoryPeerListComponent.View? { return self.storyPeerList?.view as? StoryPeerListComponent.View } @@ -848,8 +822,8 @@ public final class ChatListHeaderComponent: Component { let previousComponent = self.component self.component = component + var primaryContentTransition = transition if var primaryContent = component.primaryContent { - var primaryContentTransition = transition let primaryContentView: ContentView if let current = self.primaryContentView { primaryContentView = current @@ -877,6 +851,8 @@ public final class ChatListHeaderComponent: Component { ) self.primaryContentView = primaryContentView self.addSubview(primaryContentView) + self.leftButtonsContainer.addSubview(primaryContentView.leftButtonsContainer) + self.rightButtonsContainer.addSubview(primaryContentView.rightButtonsContainer) } let sideContentWidth: CGFloat = 0.0 @@ -889,18 +865,19 @@ public final class ChatListHeaderComponent: Component { chatListTitle: nil, leftButton: primaryContent.leftButton, rightButtons: primaryContent.rightButtons, - backTitle: primaryContent.backTitle, backPressed: primaryContent.backPressed ) } - primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) + primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, displayBackButton: primaryContent.backPressed != nil, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition) } else if let primaryContentView = self.primaryContentView { self.primaryContentView = nil primaryContentView.removeFromSuperview() + primaryContentView.leftButtonsContainer.removeFromSuperview() + primaryContentView.rightButtonsContainer.removeFromSuperview() } var storyListTransition = transition @@ -991,13 +968,16 @@ public final class ChatListHeaderComponent: Component { ) } + var secondaryContentTransition = transition + var secondaryContentIsAnimatingIn = false + var removedSecondaryContentView: ContentView? if let secondaryContent = component.secondaryContent { - var secondaryContentTransition = transition let secondaryContentView: ContentView if let current = self.secondaryContentView { secondaryContentView = current } else { secondaryContentTransition = .immediate + secondaryContentIsAnimatingIn = true secondaryContentView = ContentView( backPressed: { [weak self] in guard let self, let component = self.component else { @@ -1019,9 +999,15 @@ public final class ChatListHeaderComponent: Component { } ) self.secondaryContentView = secondaryContentView - self.addSubview(secondaryContentView) + if let primaryContentView = self.primaryContentView { + self.insertSubview(secondaryContentView, aboveSubview: primaryContentView) + } else { + self.addSubview(secondaryContentView) + } + self.leftButtonsContainer.addSubview(secondaryContentView.leftButtonsContainer) + self.rightButtonsContainer.addSubview(secondaryContentView.rightButtonsContainer) } - secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition) + secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, displayBackButton: true, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition) secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition) @@ -1032,7 +1018,7 @@ public final class ChatListHeaderComponent: Component { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: 0.0, transition: .immediate, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: 0.0, transition: .immediate, completion: {}) } } @@ -1041,26 +1027,33 @@ public final class ChatListHeaderComponent: Component { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: component.secondaryTransition, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: component.secondaryTransition, transition: transition, completion: {}) } } } else if let secondaryContentView = self.secondaryContentView { self.secondaryContentView = nil + removedSecondaryContentView = secondaryContentView if let primaryContentView = self.primaryContentView { if self.storyOffsetFraction < 0.8 { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in + secondaryContentView?.leftButtonsContainer.removeFromSuperview() + secondaryContentView?.rightButtonsContainer.removeFromSuperview() secondaryContentView?.removeFromSuperview() }) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: 0.0, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in + secondaryContentView?.leftButtonsContainer.removeFromSuperview() + secondaryContentView?.rightButtonsContainer.removeFromSuperview() secondaryContentView?.removeFromSuperview() }) } } else { + secondaryContentView.leftButtonsContainer.removeFromSuperview() + secondaryContentView.rightButtonsContainer.removeFromSuperview() secondaryContentView.removeFromSuperview() } } @@ -1070,18 +1063,10 @@ public final class ChatListHeaderComponent: Component { self.addSubview(storyPeerListComponentView) } - //let storyPeerListMinOffset: CGFloat = -7.0 let storyPeerListMaxOffset: CGFloat = availableSize.height + 2.0 - //let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - component.storiesFraction) + storyPeerListMaxOffset * component.storiesFraction - var storiesX: CGFloat = 0.0 - if let nextBackButtonView = self.secondaryContentView?.backButtonView { - let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: self) - let storyListTitleFrame = storyPeerListComponentView.titleFrame() - - storiesX += (backButtonTitleFrame.minX - storyListTitleFrame.minX) * component.secondaryTransition - } + storiesX -= availableSize.width * component.secondaryTransition storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: storiesX, y: storyPeerListMaxOffset), size: CGSize(width: availableSize.width, height: 79.0))) @@ -1090,6 +1075,84 @@ public final class ChatListHeaderComponent: Component { let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha) } + + var leftButtonsEffectiveWidth: CGFloat = 0.0 + var rightButtonsEffectiveWidth: CGFloat = 0.0 + if let primaryContentView = self.primaryContentView, let secondaryContentView = self.secondaryContentView { + + leftButtonsEffectiveWidth = primaryContentView.leftButtonsWidth * (1.0 - component.secondaryTransition) + secondaryContentView.leftButtonsWidth * component.secondaryTransition + rightButtonsEffectiveWidth = primaryContentView.rightButtonsWidth * (1.0 - component.secondaryTransition) + secondaryContentView.rightButtonsWidth * component.secondaryTransition + + primaryContentTransition.setFrame(view: primaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.leftButtonsWidth), height: 44.0))) + secondaryContentTransition.setFrame(view: secondaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.leftButtonsWidth), height: 44.0))) + + primaryContentTransition.setFrame(view: primaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - primaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.rightButtonsWidth), height: 44.0))) + + if secondaryContentIsAnimatingIn { + secondaryContentView.rightButtonsContainer.frame = CGRect(origin: CGPoint(x: self.rightButtonsContainer.bounds.width - secondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.rightButtonsWidth), height: 44.0)) + } + transition.setFrame(view: secondaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - secondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.rightButtonsWidth), height: 44.0))) + } else if let primaryContentView = self.primaryContentView { + leftButtonsEffectiveWidth = primaryContentView.leftButtonsWidth + rightButtonsEffectiveWidth = primaryContentView.rightButtonsWidth + + primaryContentTransition.setFrame(view: primaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.leftButtonsWidth), height: 44.0))) + primaryContentTransition.setFrame(view: primaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - primaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.rightButtonsWidth), height: 44.0))) + + if let removedSecondaryContentView { + transition.setFrame(view: removedSecondaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - removedSecondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, removedSecondaryContentView.rightButtonsWidth), height: 44.0))) + } + } + + if leftButtonsEffectiveWidth != 0.0 { + let leftButtonsBackgroundContainer: GlassBackgroundView + var leftButtonsBackgroundContainerTransition = transition + if let current = self.leftButtonsBackgroundContainer { + leftButtonsBackgroundContainer = current + } else { + leftButtonsBackgroundContainerTransition = leftButtonsBackgroundContainerTransition.withAnimation(.none) + leftButtonsBackgroundContainer = GlassBackgroundView() + self.leftButtonsBackgroundContainer = leftButtonsBackgroundContainer + self.addSubview(leftButtonsBackgroundContainer) + leftButtonsBackgroundContainer.contentView.addSubview(self.leftButtonsContainer) + } + let leftButtonsContainerFrame = CGRect(origin: CGPoint(x: component.sideInset, y: 0.0), size: CGSize(width: max(44.0, leftButtonsEffectiveWidth), height: 44.0)) + leftButtonsBackgroundContainerTransition.setFrame(view: leftButtonsBackgroundContainer, frame: leftButtonsContainerFrame) + leftButtonsBackgroundContainer.update(size: leftButtonsContainerFrame.size, cornerRadius: leftButtonsContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: leftButtonsBackgroundContainerTransition) + leftButtonsBackgroundContainerTransition.setFrame(view: self.leftButtonsContainer, frame: CGRect(origin: CGPoint(), size: leftButtonsContainerFrame.size)) + } else { + if let leftButtonsBackgroundContainer = self.leftButtonsBackgroundContainer { + self.leftButtonsBackgroundContainer = nil + transition.setAlpha(view: leftButtonsBackgroundContainer, alpha: 0.0, completion: { [weak leftButtonsBackgroundContainer] _ in + leftButtonsBackgroundContainer?.removeFromSuperview() + }) + } + } + + if rightButtonsEffectiveWidth != 0.0 { + let rightButtonsBackgroundContainer: GlassBackgroundView + var rightButtonsBackgroundContainerTransition = transition + if let current = self.rightButtonsBackgroundContainer { + rightButtonsBackgroundContainer = current + } else { + rightButtonsBackgroundContainerTransition = rightButtonsBackgroundContainerTransition.withAnimation(.none) + rightButtonsBackgroundContainer = GlassBackgroundView() + self.rightButtonsBackgroundContainer = rightButtonsBackgroundContainer + self.addSubview(rightButtonsBackgroundContainer) + rightButtonsBackgroundContainer.contentView.addSubview(self.rightButtonsContainer) + } + let rightButtonsContainerFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - max(44.0, rightButtonsEffectiveWidth), y: 0.0), size: CGSize(width: max(44.0, rightButtonsEffectiveWidth), height: 44.0)) + rightButtonsBackgroundContainerTransition.setFrame(view: rightButtonsBackgroundContainer, frame: rightButtonsContainerFrame) + rightButtonsBackgroundContainer.update(size: rightButtonsContainerFrame.size, cornerRadius: rightButtonsContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: rightButtonsBackgroundContainerTransition) + rightButtonsBackgroundContainerTransition.setFrame(view: self.rightButtonsContainer, frame: CGRect(origin: CGPoint(), size: rightButtonsContainerFrame.size)) + } else { + if let rightButtonsBackgroundContainer = self.rightButtonsBackgroundContainer { + self.rightButtonsBackgroundContainer = nil + transition.setAlpha(view: rightButtonsBackgroundContainer, alpha: 0.0, completion: { [weak rightButtonsBackgroundContainer] _ in + rightButtonsBackgroundContainer?.removeFromSuperview() + }) + } + } return availableSize } @@ -1119,251 +1182,3 @@ public final class ChatListHeaderComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - -public final class NavigationButtonComponentEnvironment: Equatable { - public let theme: PresentationTheme - - public init(theme: PresentationTheme) { - self.theme = theme - } - - public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { - if lhs.theme != rhs.theme { - return false - } - return true - } -} - -public final class NavigationButtonComponent: Component { - public typealias EnvironmentType = NavigationButtonComponentEnvironment - - public enum Content: Equatable { - case text(title: String, isBold: Bool) - case more - case icon(imageName: String) - case proxy(status: ChatTitleProxyStatus) - } - - public let content: Content - public let pressed: (UIView) -> Void - public let contextAction: ((UIView, ContextGesture?) -> Void)? - - public init( - content: Content, - pressed: @escaping (UIView) -> Void, - contextAction: ((UIView, ContextGesture?) -> Void)? = nil - ) { - self.content = content - self.pressed = pressed - self.contextAction = contextAction - } - - public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { - if lhs.content != rhs.content { - return false - } - return true - } - - public final class View: HighlightTrackingButton { - private var textView: ImmediateTextView? - - private var iconView: UIImageView? - private var iconImageName: String? - - private var proxyNode: ChatTitleProxyNode? - - private var moreButton: MoreHeaderButton? - - private var component: NavigationButtonComponent? - private var theme: PresentationTheme? - - override init(frame: CGRect) { - super.init(frame: frame) - - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - - self.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if highlighted { - self.textView?.alpha = 0.6 - self.proxyNode?.alpha = 0.6 - self.iconView?.alpha = 0.6 - } else { - self.textView?.alpha = 1.0 - self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - - self.proxyNode?.alpha = 1.0 - self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - - self.iconView?.alpha = 1.0 - self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - } - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func pressed() { - self.component?.pressed(self) - } - - func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - - let theme = environment[NavigationButtonComponentEnvironment.self].value.theme - var themeUpdated = false - if self.theme !== theme { - self.theme = theme - themeUpdated = true - } - - let iconOffset: CGFloat = 4.0 - - var textString: NSAttributedString? - var imageName: String? - var proxyStatus: ChatTitleProxyStatus? - var isMore: Bool = false - - switch component.content { - case let .text(title, isBold): - textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) - case .more: - isMore = true - case let .icon(imageNameValue): - imageName = imageNameValue - case let .proxy(status): - proxyStatus = status - } - - var size = CGSize(width: 0.0, height: availableSize.height) - - if let textString = textString { - let textView: ImmediateTextView - if let current = self.textView { - textView = current - } else { - textView = ImmediateTextView() - textView.isUserInteractionEnabled = false - self.textView = textView - self.addSubview(textView) - } - - textView.attributedText = textString - let textSize = textView.updateLayout(availableSize) - size.width = textSize.width - - textView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) - } else if let textView = self.textView { - self.textView = nil - textView.removeFromSuperview() - } - - if let imageName = imageName { - let iconView: UIImageView - if let current = self.iconView { - iconView = current - } else { - iconView = UIImageView() - iconView.isUserInteractionEnabled = false - self.iconView = iconView - self.addSubview(iconView) - } - if self.iconImageName != imageName || themeUpdated { - self.iconImageName = imageName - iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.rootController.navigationBar.accentTextColor) - } - - if let iconSize = iconView.image?.size { - size.width = iconSize.width - - iconView.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) - } - } else if let iconView = self.iconView { - self.iconView = nil - iconView.removeFromSuperview() - self.iconImageName = nil - } - - if let proxyStatus = proxyStatus { - let proxyNode: ChatTitleProxyNode - if let current = self.proxyNode { - proxyNode = current - } else { - proxyNode = ChatTitleProxyNode(theme: theme) - proxyNode.isUserInteractionEnabled = false - self.proxyNode = proxyNode - self.addSubnode(proxyNode) - } - - let proxySize = CGSize(width: 30.0, height: 30.0) - size.width = proxySize.width - - proxyNode.theme = theme - proxyNode.status = proxyStatus - - proxyNode.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) - } else if let proxyNode = self.proxyNode { - self.proxyNode = nil - proxyNode.removeFromSupernode() - } - - if isMore { - let moreButton: MoreHeaderButton - if let current = self.moreButton, !themeUpdated { - moreButton = current - } else { - if let moreButton = self.moreButton { - moreButton.removeFromSupernode() - self.moreButton = nil - } - - moreButton = MoreHeaderButton(color: theme.rootController.navigationBar.buttonColor) - moreButton.isUserInteractionEnabled = true - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) - moreButton.onPressed = { [weak self] in - guard let self, let component = self.component else { - return - } - self.moreButton?.play() - component.pressed(self) - } - moreButton.contextAction = { [weak self] sourceNode, gesture in - guard let self, let component = self.component else { - return - } - self.moreButton?.play() - component.contextAction?(self, gesture) - } - self.moreButton = moreButton - self.addSubnode(moreButton) - } - - let buttonSize = CGSize(width: 26.0, height: 44.0) - size.width = buttonSize.width - - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) - - moreButton.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - buttonSize.height) / 2.0)), size: buttonSize) - } else if let moreButton = self.moreButton { - self.moreButton = nil - moreButton.removeFromSupernode() - } - - return size - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 549c6fb4fd..dc26cb74e3 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -9,6 +9,7 @@ import SearchUI import AccountContext import TelegramCore import StoryPeerListComponent +import EdgeEffect public final class ChatListNavigationBar: Component { public final class AnimationHint { @@ -20,14 +21,22 @@ public final class ChatListNavigationBar: Component { self.crossfadeStoryPeers = crossfadeStoryPeers } } + + public struct Search: Equatable { + public var isEnabled: Bool + + public init(isEnabled: Bool) { + self.isEnabled = isEnabled + } + } public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings public let statusBarHeight: CGFloat public let sideInset: CGFloat + public let search: Search? public let isSearchActive: Bool - public let isSearchEnabled: Bool public let primaryContent: ChatListHeaderComponent.Content? public let secondaryContent: ChatListHeaderComponent.Content? public let secondaryTransition: CGFloat @@ -48,8 +57,8 @@ public final class ChatListNavigationBar: Component { strings: PresentationStrings, statusBarHeight: CGFloat, sideInset: CGFloat, + search: Search?, isSearchActive: Bool, - isSearchEnabled: Bool, primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?, secondaryTransition: CGFloat, @@ -69,8 +78,8 @@ public final class ChatListNavigationBar: Component { self.strings = strings self.statusBarHeight = statusBarHeight self.sideInset = sideInset + self.search = search self.isSearchActive = isSearchActive - self.isSearchEnabled = isSearchEnabled self.primaryContent = primaryContent self.secondaryContent = secondaryContent self.secondaryTransition = secondaryTransition @@ -102,10 +111,10 @@ public final class ChatListNavigationBar: Component { if lhs.sideInset != rhs.sideInset { return false } - if lhs.isSearchActive != rhs.isSearchActive { + if lhs.search != rhs.search { return false } - if lhs.isSearchEnabled != rhs.isSearchEnabled { + if lhs.isSearchActive != rhs.isSearchActive { return false } if lhs.primaryContent != rhs.primaryContent { @@ -155,8 +164,7 @@ public final class ChatListNavigationBar: Component { }() public final class View: UIView { - private let backgroundView: BlurredBackgroundView - private let separatorLayer: SimpleLayer + private let edgeEffectView: EdgeEffectView public let headerContent = ComponentView() @@ -184,17 +192,15 @@ public final class ChatListNavigationBar: Component { private var disappearingTabsViewSearch: Bool = false private var currentHeaderComponent: ChatListHeaderComponent? + + private var currentHeight: CGFloat = 0.0 override public init(frame: CGRect) { - self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) - self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0) - self.separatorLayer = SimpleLayer() - self.separatorLayer.anchorPoint = CGPoint() + self.edgeEffectView = EdgeEffectView() super.init(frame: frame) - self.addSubview(self.backgroundView) - self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.edgeEffectView) } required init?(coder: NSCoder) { @@ -202,7 +208,7 @@ public final class ChatListNavigationBar: Component { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.backgroundView.frame.contains(point) { + if point.y >= self.currentHeight { return nil } @@ -248,7 +254,12 @@ public final class ChatListNavigationBar: Component { let searchOffsetDistance: CGFloat = ChatListNavigationBar.searchScrollHeight - let minContentOffset: CGFloat = ChatListNavigationBar.searchScrollHeight + let minContentOffset: CGFloat + if component.search != nil { + minContentOffset = ChatListNavigationBar.searchScrollHeight + } else { + minContentOffset = 0.0 + } let clippedScrollOffset = min(minContentOffset, offset) if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset && !forceUpdate && !allowAvatarsExpansionUpdated { @@ -259,72 +270,87 @@ public final class ChatListNavigationBar: Component { let visibleSize = CGSize(width: currentLayout.size.width, height: max(0.0, currentLayout.size.height - clippedScrollOffset)) - let previousHeight = self.separatorLayer.position.y + let previousHeight = self.currentHeight - self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition) + /*self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition) transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: visibleSize.width, height: 1000.0))) transition.animatePosition(view: self.backgroundView, from: CGPoint(x: 0.0, y: -visibleSize.height + self.backgroundView.layer.position.y), to: CGPoint(), additive: true) - self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height) + self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height)*/ + + let edgeEffectHeight: CGFloat = currentLayout.size.height + 20.0 + var edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: currentLayout.size.width, height: edgeEffectHeight)) + if component.isSearchActive { + edgeEffectFrame.origin.y -= 46.0 + } + transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) + self.edgeEffectView.update(content: component.theme.list.plainBackgroundColor, blur: true, alpha: 0.75, rect: edgeEffectFrame, edge: .top, edgeSize: 40.0, transition: transition) - transition.setFrameWithAdditivePosition(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel))) + self.currentHeight = visibleSize.height - let searchContentNode: NavigationBarSearchContentNode - if let current = self.searchContentNode { - searchContentNode = current - - if themeUpdated { + if let search = component.search { + let searchContentNode: NavigationBarSearchContentNode + if let current = self.searchContentNode { + searchContentNode = current + + if themeUpdated { + let placeholder: String + let compactPlaceholder: String + + placeholder = component.strings.Common_Search + compactPlaceholder = component.strings.Common_Search + + searchContentNode.updateThemeAndPlaceholder(theme: component.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder) + } + } else { let placeholder: String let compactPlaceholder: String placeholder = component.strings.Common_Search compactPlaceholder = component.strings.Common_Search - searchContentNode.updateThemeAndPlaceholder(theme: component.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder) - } - } else { - let placeholder: String - let compactPlaceholder: String - - placeholder = component.strings.Common_Search - compactPlaceholder = component.strings.Common_Search - - searchContentNode = NavigationBarSearchContentNode( - theme: component.theme, - placeholder: placeholder, - compactPlaceholder: compactPlaceholder, - activate: { [weak self] in - guard let self, let component = self.component, let searchContentNode = self.searchContentNode else { - return + searchContentNode = NavigationBarSearchContentNode( + theme: component.theme, + placeholder: placeholder, + compactPlaceholder: compactPlaceholder, + activate: { [weak self] in + guard let self, let component = self.component, let searchContentNode = self.searchContentNode else { + return + } + component.activateSearch(searchContentNode) } - component.activateSearch(searchContentNode) - } - ) - searchContentNode.view.layer.anchorPoint = CGPoint() - self.searchContentNode = searchContentNode - self.addSubview(searchContentNode.view) + ) + searchContentNode.view.layer.anchorPoint = CGPoint() + self.searchContentNode = searchContentNode + self.addSubview(searchContentNode.view) + } + + let searchSize = CGSize(width: currentLayout.size.width - 6.0 * 2.0, height: navigationBarSearchContentHeight) + var searchFrame = CGRect(origin: CGPoint(x: 6.0, y: visibleSize.height - searchSize.height), size: searchSize) + if component.tabsNode != nil { + searchFrame.origin.y -= 40.0 + } + if !component.isSearchActive { + searchFrame.origin.y -= component.accessoryPanelContainerHeight + } + + let clippedSearchOffset = max(0.0, min(clippedScrollOffset, searchOffsetDistance)) + let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance + searchContentNode.expansionProgress = 1.0 - searchOffsetFraction + + transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame) + + searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) + + transition.setAlpha(view: searchContentNode.view, alpha: search.isEnabled ? 1.0 : 0.5) + searchContentNode.isUserInteractionEnabled = search.isEnabled + } else { + if let searchContentNode = self.searchContentNode { + self.searchContentNode = nil + searchContentNode.view.removeFromSuperview() + } } - let searchSize = CGSize(width: currentLayout.size.width - 6.0 * 2.0, height: navigationBarSearchContentHeight) - var searchFrame = CGRect(origin: CGPoint(x: 6.0, y: visibleSize.height - searchSize.height), size: searchSize) - if component.tabsNode != nil { - searchFrame.origin.y -= 40.0 - } - if !component.isSearchActive { - searchFrame.origin.y -= component.accessoryPanelContainerHeight - } - - let clippedSearchOffset = max(0.0, min(clippedScrollOffset, searchOffsetDistance)) - let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance - searchContentNode.expansionProgress = 1.0 - searchOffsetFraction - - transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame) - - searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) - - transition.setAlpha(view: searchContentNode.view, alpha: component.isSearchEnabled ? 1.0 : 0.5) - searchContentNode.isUserInteractionEnabled = component.isSearchEnabled - let headerTransition = transition let storiesOffsetFraction: CGFloat @@ -455,7 +481,8 @@ public final class ChatListNavigationBar: Component { } var tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: 46.0)) - if !component.isSearchActive { + if component.isSearchActive { + } else { tabsFrame.origin.y -= component.accessoryPanelContainerHeight } if component.tabsNode != nil { @@ -463,7 +490,8 @@ public final class ChatListNavigationBar: Component { } var accessoryPanelContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: component.accessoryPanelContainerHeight)) - if !component.isSearchActive { + if component.isSearchActive { + } else { accessoryPanelContainerFrame.origin.y -= component.accessoryPanelContainerHeight } @@ -528,8 +556,8 @@ public final class ChatListNavigationBar: Component { strings: component.strings, statusBarHeight: component.statusBarHeight, sideInset: component.sideInset, + search: component.search, isSearchActive: component.isSearchActive, - isSearchEnabled: component.isSearchEnabled, primaryContent: component.primaryContent, secondaryContent: component.secondaryContent, secondaryTransition: component.secondaryTransition, @@ -575,8 +603,6 @@ public final class ChatListNavigationBar: Component { } func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme - var uploadProgressUpdated = false var storySubscriptionsUpdated = false if let previousComponent = self.component { @@ -591,23 +617,20 @@ public final class ChatListNavigationBar: Component { self.component = component self.state = state - if themeUpdated { - self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor - } - var contentHeight = component.statusBarHeight if component.statusBarHeight >= 1.0 { contentHeight += 3.0 } contentHeight += 44.0 + contentHeight += 8.0 if component.isSearchActive { if component.statusBarHeight < 1.0 { contentHeight += 8.0 } - } else { + } + if component.search != nil { contentHeight += navigationBarSearchContentHeight } @@ -615,8 +638,11 @@ public final class ChatListNavigationBar: Component { contentHeight += 40.0 } - if component.accessoryPanelContainer != nil && !component.isSearchActive { - contentHeight += component.accessoryPanelContainerHeight + if component.isSearchActive { + } else { + if component.accessoryPanelContainer != nil { + contentHeight += component.accessoryPanelContainerHeight + } } let size = CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift new file mode 100644 index 0000000000..4027524c18 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift @@ -0,0 +1,254 @@ +import Foundation +import UIKit +import ComponentFlow +import ChatListTitleView +import TelegramPresentationData +import Display +import MoreHeaderButton + +public final class NavigationButtonComponentEnvironment: Equatable { + public let theme: PresentationTheme + + public init(theme: PresentationTheme) { + self.theme = theme + } + + public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { + if lhs.theme != rhs.theme { + return false + } + return true + } +} + +public final class NavigationButtonComponent: Component { + public typealias EnvironmentType = NavigationButtonComponentEnvironment + + public enum Content: Equatable { + case text(title: String, isBold: Bool) + case more + case icon(imageName: String) + case proxy(status: ChatTitleProxyStatus) + } + + public let content: Content + public let pressed: (UIView) -> Void + public let contextAction: ((UIView, ContextGesture?) -> Void)? + + public init( + content: Content, + pressed: @escaping (UIView) -> Void, + contextAction: ((UIView, ContextGesture?) -> Void)? = nil + ) { + self.content = content + self.pressed = pressed + self.contextAction = contextAction + } + + public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var textView: ImmediateTextView? + + private var iconView: UIImageView? + private var iconImageName: String? + + private var proxyNode: ChatTitleProxyNode? + + private var moreButton: MoreHeaderButton? + + private var component: NavigationButtonComponent? + private var theme: PresentationTheme? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.textView?.alpha = 0.6 + self.proxyNode?.alpha = 0.6 + self.iconView?.alpha = 0.6 + } else { + self.textView?.alpha = 1.0 + self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.proxyNode?.alpha = 1.0 + self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.iconView?.alpha = 1.0 + self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.pressed(self) + } + + func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let theme = environment[NavigationButtonComponentEnvironment.self].value.theme + var themeUpdated = false + if self.theme !== theme { + self.theme = theme + themeUpdated = true + } + + var textString: NSAttributedString? + var imageName: String? + var proxyStatus: ChatTitleProxyStatus? + var isMore: Bool = false + + switch component.content { + case let .text(title, isBold): + textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlColor) + case .more: + isMore = true + case let .icon(imageNameValue): + imageName = imageNameValue + case let .proxy(status): + proxyStatus = status + } + + var size = CGSize(width: 0.0, height: availableSize.height) + + if let textString = textString { + let textView: ImmediateTextView + if let current = self.textView { + textView = current + } else { + textView = ImmediateTextView() + textView.isUserInteractionEnabled = false + self.textView = textView + self.addSubview(textView) + } + + textView.attributedText = textString + let textSize = textView.updateLayout(availableSize) + let textInset: CGFloat = 10.0 + size.width = max(44.0, textSize.width + textInset * 2.0) + + textView.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) + } else if let textView = self.textView { + self.textView = nil + textView.removeFromSuperview() + } + + if let imageName = imageName { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + iconView = UIImageView() + iconView.isUserInteractionEnabled = false + self.iconView = iconView + self.addSubview(iconView) + } + if self.iconImageName != imageName || themeUpdated { + self.iconImageName = imageName + iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.chat.inputPanel.panelControlColor) + } + + if let iconSize = iconView.image?.size { + size.width = 44.0 + + iconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) + } + } else if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + self.iconImageName = nil + } + + if let proxyStatus = proxyStatus { + let proxyNode: ChatTitleProxyNode + if let current = self.proxyNode { + proxyNode = current + } else { + proxyNode = ChatTitleProxyNode(theme: theme) + proxyNode.isUserInteractionEnabled = false + self.proxyNode = proxyNode + self.addSubnode(proxyNode) + } + + let proxySize = CGSize(width: 30.0, height: 30.0) + size.width = 44.0 + + proxyNode.theme = theme + proxyNode.status = proxyStatus + + proxyNode.frame = CGRect(origin: CGPoint(x: floor((size.width - proxySize.width) / 2.0), y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) + } else if let proxyNode = self.proxyNode { + self.proxyNode = nil + proxyNode.removeFromSupernode() + } + + if isMore { + let moreButton: MoreHeaderButton + if let current = self.moreButton, !themeUpdated { + moreButton = current + } else { + if let moreButton = self.moreButton { + moreButton.removeFromSupernode() + self.moreButton = nil + } + + moreButton = MoreHeaderButton(color: theme.chat.inputPanel.panelControlColor) + moreButton.isUserInteractionEnabled = true + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.chat.inputPanel.panelControlColor))) + moreButton.onPressed = { [weak self] in + guard let self, let component = self.component else { + return + } + self.moreButton?.play() + component.pressed(self) + } + moreButton.contextAction = { [weak self] sourceNode, gesture in + guard let self, let component = self.component else { + return + } + self.moreButton?.play() + component.contextAction?(self, gesture) + } + self.moreButton = moreButton + self.addSubnode(moreButton) + } + + let buttonSize = CGSize(width: 30.0, height: 44.0) + size.width = 44.0 + + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) + + moreButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + } else if let moreButton = self.moreButton { + self.moreButton = nil + moreButton.removeFromSupernode() + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift new file mode 100644 index 0000000000..bdc1e32e4e --- /dev/null +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift @@ -0,0 +1,6 @@ +import Foundation +import UIKit +import ComponentFlow + +final class NavigationButtonContainerView: UIView { +} diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift index 5ec474b50d..b3d2606b47 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift @@ -44,13 +44,13 @@ public final class ChatTitleProxyNode: ASDisplayNode { if self.theme !== oldValue { switch self.status { case .connecting: - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: false) case .connected: - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: true, off: false) case .available: - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) } - self.activityIndicator.type = .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.3333, true) + self.activityIndicator.type = .custom(theme.chat.inputPanel.panelControlColor, 10.0, 1.3333, true) } } } @@ -61,13 +61,13 @@ public final class ChatTitleProxyNode: ASDisplayNode { switch self.status { case .connecting: self.activityIndicator.isHidden = false - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: false) case .connected: self.activityIndicator.isHidden = true - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: true, off: false) case .available: self.activityIndicator.isHidden = true - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) } } } @@ -80,9 +80,9 @@ public final class ChatTitleProxyNode: ASDisplayNode { self.iconNode.isLayerBacked = true self.iconNode.displayWithoutProcessing = true self.iconNode.displaysAsynchronously = false - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) - self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.3333, true), speed: .slow) + self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlColor, 10.0, 1.3333, true), speed: .slow) super.init() diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index e5d1e84948..040181f130 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -64,13 +64,15 @@ public final class LiquidLensView: UIView { var selectionWidth: CGFloat var isDark: Bool var isLifted: Bool + var isCollapsed: Bool - init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool) { + init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { self.size = size self.selectionX = selectionX self.selectionWidth = selectionWidth self.isLifted = isLifted self.isDark = isDark + self.isCollapsed = isCollapsed } } @@ -85,8 +87,8 @@ public final class LiquidLensView: UIView { } private let containerView: UIView - private let backgroundContainerContainer: UIView - private let backgroundContainer: GlassBackgroundContainerView + private let backgroundContainer: GlassBackgroundContainerView? + private let genericBackgroundContainer: UIView? private let backgroundView: GlassBackgroundView private var lensView: UIView? private let liftedContainerView: UIView @@ -117,11 +119,16 @@ public final class LiquidLensView: UIView { return self.params?.selectionWidth } - override public init(frame: CGRect) { + public init(useBackgroundContainer: Bool = true) { self.containerView = UIView() - self.backgroundContainerContainer = UIView() - self.backgroundContainer = GlassBackgroundContainerView() + if useBackgroundContainer { + self.backgroundContainer = GlassBackgroundContainerView() + self.genericBackgroundContainer = nil + } else { + self.backgroundContainer = nil + self.genericBackgroundContainer = UIView() + } self.backgroundView = GlassBackgroundView() @@ -130,12 +137,16 @@ public final class LiquidLensView: UIView { self.restingBackgroundView = RestingBackgroundView() - super.init(frame: frame) + super.init(frame: CGRect()) - self.backgroundContainerContainer.addSubview(self.backgroundContainer) - self.addSubview(self.backgroundContainerContainer) + if let backgroundContainer = self.backgroundContainer { + self.addSubview(backgroundContainer) + backgroundContainer.contentView.addSubview(self.backgroundView) + } else if let genericBackgroundContainer = self.genericBackgroundContainer { + self.addSubview(genericBackgroundContainer) + genericBackgroundContainer.addSubview(self.backgroundView) + } - self.backgroundContainer.contentView.addSubview(self.backgroundView) self.backgroundView.contentView.addSubview(self.containerView) self.containerView.isUserInteractionEnabled = false @@ -150,7 +161,11 @@ public final class LiquidLensView: UIView { } if let lensView = self.lensView { - self.backgroundContainer.layer.zPosition = 1 + if let backgroundContainer = self.backgroundContainer { + backgroundContainer.layer.zPosition = 1 + } else if let genericBackgroundContainer = self.genericBackgroundContainer{ + genericBackgroundContainer.layer.zPosition = 1 + } lensView.layer.zPosition = 10.0 self.liftedContainerView.addSubview(self.restingBackgroundView) @@ -159,7 +174,11 @@ public final class LiquidLensView: UIView { self.containerView.addSubview(lensView) self.containerView.addSubview(self.contentView) - lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: self.backgroundContainer.contentView) + if let backgroundContainer = self.backgroundContainer { + lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: backgroundContainer.contentView) + } else if let genericBackgroundContainer = self.genericBackgroundContainer { + lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: genericBackgroundContainer) + } lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView) lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView) @@ -223,8 +242,8 @@ public final class LiquidLensView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, transition: ComponentTransition) { - let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted) + public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { + let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) if self.params == params { return } @@ -319,16 +338,36 @@ public final class LiquidLensView: UIView { self.params = params transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: params.size)) - transition.setFrame(view: self.backgroundContainerContainer, frame: CGRect(origin: CGPoint(), size: params.size)) - transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) - self.backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition) + if let backgroundContainer = self.backgroundContainer { + transition.setFrame(view: backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) + backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition) + } else if let genericBackgroundContainer = self.genericBackgroundContainer { + transition.setFrame(view: genericBackgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) + } transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size)) - transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size)) + if self.contentView.bounds.size != params.size { + self.contentView.clipsToBounds = true + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in + guard let self, completed else { + return + } + self.contentView.clipsToBounds = false + }) + transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: params.size.height * 0.5) + + self.liftedContainerView.clipsToBounds = true + transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in + guard let self, completed else { + return + } + self.liftedContainerView.clipsToBounds = false + }) + transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.size.height * 0.5) + } let baseLensFrame = CGRect(origin: CGPoint(x: max(0.0, min(params.selectionX, params.size.width - params.selectionWidth)), y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height)) self.updateLens(params: LensParams(baseFrame: baseLensFrame, isLifted: params.isLifted), animated: !transition.animation.isImmediate) @@ -354,7 +393,7 @@ public final class LiquidLensView: UIView { transition.setFrame(view: self.restingBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.restingBackgroundView.update(isDark: params.isDark) - transition.setAlpha(view: self.restingBackgroundView, alpha: params.isLifted ? 0.0 : 1.0) + transition.setAlpha(view: self.restingBackgroundView, alpha: (params.isLifted || params.isCollapsed) ? 0.0 : 1.0) if params.isLifted { if self.liftedDisplayLink == nil { diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift index 579bd7b8d9..5cb901cfde 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -266,8 +266,7 @@ final class MiniAppListScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backTitle: isModal ? nil : strings.Common_Back, - backPressed: { [weak self] in + backPressed: isModal ? nil : { [weak self] in guard let self else { return } @@ -286,8 +285,8 @@ final class MiniAppListScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, + search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index ad318df6ff..c05923ba09 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -13923,6 +13923,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } self._readyProxy.set(.single(true)) }) + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required init(coder aDecoder: NSCoder) { @@ -14476,7 +14478,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar { self.addSubnode(bottomNavigationBar.additionalContentNode) - if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { + /*if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { if let previousBackButtonArrow = headerView.makeTransitionBackArrowView(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { self.previousBackButtonArrow = previousBackButtonArrow self.view.addSubview(previousBackButtonArrow) @@ -14485,7 +14487,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig self.previousBackButton = previousBackButton self.view.addSubview(previousBackButton) } - } else { + } else*/ do { if let previousBackButtonArrow = bottomNavigationBar.makeTransitionBackArrowView(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { self.previousBackButtonArrow = previousBackButtonArrow self.view.addSubview(previousBackButtonArrow) @@ -14594,13 +14596,13 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } if let previousBackButton = self.previousBackButton { - if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { + /*if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { if let backButtonTitleView = headerView.backButtonTitleView { let previousBackButtonFrame = backButtonTitleView.convert(backButtonTitleView.bounds, to: bottomNavigationBar.view) previousBackButton.frame = previousBackButtonFrame transition.updateAlpha(layer: previousBackButton.layer, alpha: fraction) } - } else { + } else*/ do { let previousBackButtonFrame = bottomNavigationBar.backButtonNode.view.convert(bottomNavigationBar.backButtonNode.view.bounds, to: bottomNavigationBar.view) previousBackButton.frame = previousBackButtonFrame transition.updateAlpha(layer: previousBackButton.layer, alpha: fraction) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/BUILD b/submodules/TelegramUI/Components/PeerSelectionController/BUILD index 230b96365d..f2c10542eb 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/BUILD +++ b/submodules/TelegramUI/Components/PeerSelectionController/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode", "//submodules/CounterControllerTitleView", + "//submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index db095daadd..0dc5f6b517 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -10,6 +10,7 @@ import AccountContext import SearchUI import ChatListUI import CounterControllerTitleView +import ChatListFilterTabContainerNode public final class PeerSelectionControllerImpl: ViewController, PeerSelectionController { private let context: AccountContext diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 67c0665476..0114593bb9 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -798,8 +798,7 @@ final class QuickReplySetupScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backTitle: isModal ? nil : strings.Common_Back, - backPressed: { [weak self] in + backPressed: isModal ? nil :{ [weak self] in guard let self else { return } @@ -818,8 +817,8 @@ final class QuickReplySetupScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, + search: ChatListNavigationBar.Search(isEnabled: !self.isEditing), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: !self.isEditing, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index b99ea5e371..8aadf7da54 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -298,8 +298,7 @@ final class PeerSelectionScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backTitle: isModal ? nil : strings.Common_Back, - backPressed: { [weak self] in + backPressed: isModal ? nil : { [weak self] in guard let self else { return } @@ -318,8 +317,8 @@ final class PeerSelectionScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, + search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/TabBarComponent/BUILD b/submodules/TelegramUI/Components/TabBarComponent/BUILD index bf1656c7b3..e08cc9e22b 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/BUILD +++ b/submodules/TelegramUI/Components/TabBarComponent/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/UIKitRuntimeUtils", "//submodules/TelegramUI/Components/LiquidLens", "//submodules/AppBundle", + "//submodules/SearchBarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 585e15ed8c..010a270d59 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -12,6 +12,7 @@ import BundleIconComponent import TextBadgeComponent import LiquidLens import AppBundle +import SearchBarNode private final class TabSelectionRecognizer: UIGestureRecognizer { private var initialLocation: CGPoint? @@ -69,35 +70,147 @@ private final class TabSelectionRecognizer: UIGestureRecognizer { } } -public final class TabBarSearchView: UIView { - private let backgroundView: GlassBackgroundView - private let iconView: GlassBackgroundView.ContentImageView - - override public init(frame: CGRect) { - self.backgroundView = GlassBackgroundView() - self.iconView = GlassBackgroundView.ContentImageView() +public final class NavigationSearchView: UIView { + private struct Params: Equatable { + let size: CGSize + let theme: PresentationTheme + let strings: PresentationStrings + let isActive: Bool - super.init(frame: frame) + init(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool) { + self.size = size + self.theme = theme + self.strings = strings + self.isActive = isActive + } + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.size != rhs.size { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.isActive != rhs.isActive { + return false + } + return true + } + } + + private let action: () -> Void + + private let backgroundView: GlassBackgroundView + private let iconView: UIImageView + private(set) var searchBarNode: SearchBarNode? + + private var params: Params? + + public init(action: @escaping () -> Void) { + self.action = action + + self.backgroundView = GlassBackgroundView() + self.backgroundView.contentView.clipsToBounds = true + self.iconView = UIImageView() + + super.init(frame: CGRect()) self.addSubview(self.backgroundView) self.backgroundView.contentView.addSubview(self.iconView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + } + + @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.action() + } } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, isDark: Bool, tintColor: GlassBackgroundView.TintColor, iconColor: UIColor, transition: ComponentTransition) { - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, tintColor: tintColor, transition: transition) + public func update(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool, transition: ComponentTransition) { + let params = Params(size: size, theme: theme, strings: strings, isActive: isActive) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(params: Params, transition: ComponentTransition) { + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + + self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) if self.iconView.image == nil { self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate) } - self.iconView.tintColor = iconColor + transition.setTintColor(view: self.iconView, color: params.isActive ? params.theme.rootController.navigationSearchBar.inputIconColor : params.theme.chat.inputPanel.panelControlColor) if let image = self.iconView.image { - transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)) + let imageSize: CGSize + let iconFrame: CGRect + if params.isActive { + let iconFraction: CGFloat = 0.8 + imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction) + iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((params.size.height - imageSize.height) * 0.5)), size: imageSize) + } else { + iconFrame = CGRect(origin: CGPoint(x: floor((params.size.width - image.size.width) * 0.5), y: floor((params.size.height - image.size.height) * 0.5)), size: image.size) + } + transition.setPosition(view: self.iconView, position: iconFrame.center) + transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + } + + if params.isActive { + let searchBarNode: SearchBarNode + var searchBarNodeTransition = transition + if let current = self.searchBarNode { + searchBarNode = current + } else { + searchBarNodeTransition = searchBarNodeTransition.withAnimation(.none) + searchBarNode = SearchBarNode( + theme: SearchBarNodeTheme( + background: .clear, + separator: .clear, + inputFill: .clear, + primaryText: params.theme.chat.inputPanel.panelControlColor, + placeholder: params.theme.chat.inputPanel.inputPlaceholderColor, + inputIcon: params.theme.chat.inputPanel.inputControlColor, + inputClear: params.theme.chat.inputPanel.inputControlColor, + accent: params.theme.chat.inputPanel.panelControlAccentColor, + keyboard: params.theme.rootController.keyboardColor + ), + strings: params.strings, + fieldStyle: .inlineNavigation, + icon: .loupe, + forceSeparator: false, + displayBackground: false, + cancelText: nil + ) + searchBarNode.placeholderString = NSAttributedString(string: params.strings.Common_Search, font: Font.regular(17.0), textColor: params.theme.chat.inputPanel.inputPlaceholderColor) + self.searchBarNode = searchBarNode + self.backgroundView.contentView.addSubview(searchBarNode.view) + searchBarNode.view.alpha = 0.0 + } + let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: params.size.width - 36.0 - 4.0, height: params.size.height)) + transition.setFrame(view: searchBarNode.view, frame: searchBarFrame) + searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition) + alphaTransition.setAlpha(view: searchBarNode.view, alpha: 1.0) + } else { + if let searchBarNode = self.searchBarNode { + self.searchBarNode = nil + let searchBarNodeView = searchBarNode.view + alphaTransition.setAlpha(view: searchBarNode.view, alpha: 0.0, completion: { [weak searchBarNodeView] _ in + searchBarNodeView?.removeFromSuperview() + }) + } } } } @@ -131,20 +244,45 @@ public final class TabBarComponent: Component { return true } } + + public final class Search: Equatable { + public let isActive: Bool + public let activate: () -> Void + public let deactivate: () -> Void + + public init(isActive: Bool, activate: @escaping () -> Void, deactivate: @escaping () -> Void) { + self.isActive = isActive + self.activate = activate + self.deactivate = deactivate + } + + public static func ==(lhs: Search, rhs: Search) -> Bool { + if lhs.isActive != rhs.isActive { + return false + } + return true + } + } public let theme: PresentationTheme + public let strings: PresentationStrings public let items: [Item] + public let search: Search? public let selectedId: AnyHashable? public let isTablet: Bool public init( theme: PresentationTheme, + strings: PresentationStrings, items: [Item], + search: Search?, selectedId: AnyHashable?, isTablet: Bool ) { self.theme = theme + self.strings = strings self.items = items + self.search = search self.selectedId = selectedId self.isTablet = isTablet } @@ -153,9 +291,15 @@ public final class TabBarComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.strings !== rhs.strings { + return false + } if lhs.items != rhs.items { return false } + if lhs.search != rhs.search { + return false + } if lhs.selectedId != rhs.selectedId { return false } @@ -166,11 +310,14 @@ public final class TabBarComponent: Component { } public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate { + private let backgroundContainer: GlassBackgroundContainerView private let liquidLensView: LiquidLensView private let contextGestureContainerView: ContextControllerSourceView private var itemViews: [AnyHashable: ComponentView] = [:] private var selectedItemViews: [AnyHashable: ComponentView] = [:] + + private var searchView: NavigationSearchView? private var tabSelectionRecognizer: TabSelectionRecognizer? private var itemWithActiveContextGesture: AnyHashable? @@ -178,11 +325,16 @@ public final class TabBarComponent: Component { private var component: TabBarComponent? private weak var state: EmptyComponentState? - private var selectionGestureState: (startX: CGFloat, currentX: CGFloat)? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? private var overrideSelectedItemId: AnyHashable? + + public var currentSearchNode: ASDisplayNode? { + return self.searchView?.searchBarNode + } public override init(frame: CGRect) { - self.liquidLensView = LiquidLensView() + self.backgroundContainer = GlassBackgroundContainerView() + self.liquidLensView = LiquidLensView(useBackgroundContainer: false) self.contextGestureContainerView = ContextControllerSourceView() self.contextGestureContainerView.isGestureEnabled = true @@ -194,12 +346,13 @@ public final class TabBarComponent: Component { self.traitOverrides.horizontalSizeClass = .compact } - self.addSubview(self.contextGestureContainerView) + self.addSubview(self.backgroundContainer) + self.backgroundContainer.contentView.addSubview(self.contextGestureContainerView) self.contextGestureContainerView.addSubview(self.liquidLensView) let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) self.tabSelectionRecognizer = tabSelectionRecognizer - self.addGestureRecognizer(tabSelectionRecognizer) + self.contextGestureContainerView.addGestureRecognizer(tabSelectionRecognizer) self.contextGestureContainerView.shouldBegin = { [weak self] point in guard let self, let component = self.component else { @@ -281,29 +434,41 @@ public final class TabBarComponent: Component { } @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } switch recognizer.state { case .began: - if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view { + if let search = component.search, search.isActive { + } else if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view { let startX = itemView.frame.minX - 4.0 - self.selectionGestureState = (startX, startX) + self.selectionGestureState = (startX, startX, itemId) self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } case .changed: - if var selectionGestureState = self.selectionGestureState { + if let search = component.search, search.isActive { + } else if var selectionGestureState = self.selectionGestureState { selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: recognizer.location(in: self)) { + selectionGestureState.itemId = itemId + } self.selectionGestureState = selectionGestureState self.state?.updated(transition: .immediate, isLocal: true) } case .ended, .cancelled: - self.selectionGestureState = nil - if let component = self.component, let itemId = self.item(at: recognizer.location(in: self)) { - guard let item = component.items.first(where: { $0.id == itemId }) else { - return + if let search = component.search, search.isActive { + search.deactivate() + } else if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state, let component = self.component { + guard let item = component.items.first(where: { $0.id == selectionGestureState.itemId }) else { + return + } + self.overrideSelectedItemId = selectionGestureState.itemId + item.action(false) } - self.overrideSelectedItemId = itemId - item.action(false) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } - self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) default: break } @@ -355,22 +520,28 @@ public final class TabBarComponent: Component { } func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + let _ = alphaTransition + let innerInset: CGFloat = 4.0 let availableSize = CGSize(width: min(500.0, availableSize.width), height: availableSize.height) let previousComponent = self.component self.component = component self.state = state - - let _ = innerInset - let _ = availableSize - let _ = previousComponent self.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light - let itemSize = CGSize(width: floor((availableSize.width - innerInset * 2.0) / CGFloat(component.items.count)), height: 56.0) + let barHeight: CGFloat = 56.0 + innerInset * 2.0 + + var availableItemsWidth: CGFloat = availableSize.width - innerInset * 2.0 + if component.search != nil { + availableItemsWidth -= barHeight + 8.0 + } + + let itemSize = CGSize(width: floor(availableItemsWidth / CGFloat(component.items.count)), height: 56.0) let contentWidth: CGFloat = innerInset * 2.0 + CGFloat(component.items.count) * itemSize.width - let size = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0) + let tabsSize = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0) var validIds: [AnyHashable] = [] var selectionFrame: CGRect? @@ -409,6 +580,7 @@ public final class TabBarComponent: Component { component: AnyComponent(ItemComponent( item: item, theme: component.theme, + isCompact: component.search?.isActive == true, isSelected: false )), environment: {}, @@ -419,14 +591,21 @@ public final class TabBarComponent: Component { component: AnyComponent(ItemComponent( item: item, theme: component.theme, + isCompact: component.search?.isActive == true, isSelected: true )), environment: {}, containerSize: itemSize ) - let itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((size.height - itemSize.height) * 0.5)), size: itemSize) + var itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((tabsSize.height - itemSize.height) * 0.5)), size: itemSize) + if isItemSelected { + selectionFrame = itemFrame + } + if let itemComponentView = itemView.view as? ItemComponent.View, let selectedItemComponentView = selectedItemView.view as? ItemComponent.View { + let itemAlphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + if itemComponentView.superview == nil { itemComponentView.isUserInteractionEnabled = false selectedItemComponentView.isUserInteractionEnabled = false @@ -434,6 +613,27 @@ public final class TabBarComponent: Component { self.liquidLensView.contentView.addSubview(itemComponentView) self.liquidLensView.selectedContentView.addSubview(selectedItemComponentView) } + + if let search = component.search, search.isActive { + if isItemSelected { + itemFrame.origin.x = floor((48.0 - itemSize.width) * 0.5) + itemTransition.setAlpha(view: itemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0) + itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0) + } else { + itemTransition.setAlpha(view: itemComponentView, alpha: 0.0) + itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 10.0) + itemTransition.setAlpha(view: selectedItemComponentView, alpha: 0.0) + itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 10.0) + } + } else { + itemTransition.setAlpha(view: itemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0) + itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) itemTransition.setPosition(view: selectedItemComponentView, position: itemFrame.center) itemTransition.setBounds(view: selectedItemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) @@ -444,9 +644,6 @@ public final class TabBarComponent: Component { selectedItemComponentView.playSelectionAnimation() } } - if isItemSelected { - selectionFrame = itemFrame - } } var removeIds: [AnyHashable] = [] @@ -461,12 +658,17 @@ public final class TabBarComponent: Component { self.itemViews.removeValue(forKey: id) self.selectedItemViews.removeValue(forKey: id) } - - transition.setFrame(view: self.contextGestureContainerView, frame: CGRect(origin: CGPoint(), size: size)) - - transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: size)) - let lensSelection: (x: CGFloat, width: CGFloat) + var tabsFrame = CGRect(origin: CGPoint(), size: tabsSize) + if let search = component.search, search.isActive { + tabsFrame.size = CGSize(width: 48.0, height: 48.0) + tabsFrame.origin.y = tabsSize.height - 48.0 + } + + transition.setFrame(view: self.contextGestureContainerView, frame: tabsFrame) + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: tabsSize)) + + var lensSelection: (x: CGFloat, width: CGFloat) if let selectionGestureState = self.selectionGestureState { lensSelection = (selectionGestureState.currentX, itemSize.width + innerInset * 2.0) } else if let selectionFrame { @@ -475,7 +677,62 @@ public final class TabBarComponent: Component { lensSelection = (0.0, itemSize.width) } - self.liquidLensView.update(size: size, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, transition: transition) + var lensSize: CGSize = tabsSize + var isLensCollapsed = false + if let search = component.search, search.isActive { + isLensCollapsed = true + lensSize = CGSize(width: 48.0, height: 48.0) + lensSelection = (0.0, 48.0) + } + self.liquidLensView.update(size: lensSize, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition) + + var size = tabsSize + + if let search = component.search { + let searchSize: CGSize + let searchFrame: CGRect + if search.isActive { + size.width = availableSize.width + searchSize = CGSize(width: availableSize.width - 48.0 - 8.0, height: 48.0) + searchFrame = CGRect(origin: CGPoint(x: 48.0 + 8.0, y: size.height - searchSize.height), size: searchSize) + } else { + searchSize = CGSize(width: barHeight, height: barHeight) + size.width += barHeight + 8.0 + searchFrame = CGRect(origin: CGPoint(x: availableSize.width - searchSize.width, y: 0.0), size: searchSize) + } + + let searchView: NavigationSearchView + var searchViewTransition = transition + if let current = self.searchView { + searchView = current + } else { + searchViewTransition = searchViewTransition.withAnimation(.none) + searchView = NavigationSearchView(action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.search?.activate() + }) + self.searchView = searchView + self.backgroundContainer.contentView.addSubview(searchView) + searchView.frame = CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchSize) + } + searchView.update(size: searchSize, theme: component.theme, strings: component.strings, isActive: search.isActive, transition: searchViewTransition) + transition.setFrame(view: searchView, frame: searchFrame) + } else { + if let searchView = self.searchView { + self.searchView = nil + transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchView.bounds.size), completion: { [weak searchView] completed in + guard let searchView, completed else { + return + } + searchView.removeFromSuperview() + }) + } + } + + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundContainer.update(size: size, isDark: component.theme.overallDarkAppearance, transition: transition) return size } @@ -493,11 +750,13 @@ public final class TabBarComponent: Component { private final class ItemComponent: Component { let item: TabBarComponent.Item let theme: PresentationTheme + let isCompact: Bool let isSelected: Bool - init(item: TabBarComponent.Item, theme: PresentationTheme, isSelected: Bool) { + init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool) { self.item = item self.theme = theme + self.isCompact = isCompact self.isSelected = isSelected } @@ -508,6 +767,9 @@ private final class ItemComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.isCompact != rhs.isCompact { + return false + } if lhs.isSelected != rhs.isSelected { return false } @@ -562,6 +824,8 @@ private final class ItemComponent: Component { } func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + let previousComponent = self.component if previousComponent?.item.item !== component.item.item { @@ -619,7 +883,7 @@ private final class ItemComponent: Component { content: LottieComponent.AppBundleContent( name: animationName ), - color: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, + color: (component.isSelected && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, placeholderColor: nil, startingPosition: .end, size: CGSize(width: 48.0, height: 48.0), @@ -692,6 +956,7 @@ private final class ItemComponent: Component { self.contextContainerView.contentView.addSubview(titleView) } titleView.frame = titleFrame + alphaTransition.setAlpha(view: titleView, alpha: component.isCompact ? 0.0 : 1.0) } if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty { @@ -723,6 +988,7 @@ private final class ItemComponent: Component { self.contextContainerView.contentView.addSubview(badgeView) } badgeTransition.setFrame(view: badgeView, frame: badgeFrame) + alphaTransition.setAlpha(view: badgeView, alpha: component.isCompact ? 0.0 : 1.0) } } else if let badge = self.badge { self.badge = nil diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index c0f1427eee..2abea859eb 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -188,7 +188,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } public func addRootControllers(showCallsTab: Bool) { - let tabBarController = TabBarControllerImpl(theme: self.presentationData.theme) + let tabBarController = TabBarControllerImpl(theme: self.presentationData.theme, strings: self.presentationData.strings) tabBarController.navigationPresentation = .master let chatListController = self.context.sharedContext.makeChatListController(context: self.context, location: .chatList(groupId: .root), controlsHistoryPreload: true, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: !GlobalExperimentalSettings.isAppStoreBuild) if let sharedContext = self.context.sharedContext as? SharedAccountContextImpl {