From f3e75ca2e3ad0c93008ee96cf9884d5f79f76fa8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 May 2024 18:15:14 +0400 Subject: [PATCH 1/8] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 15 + .../Sources/AccountContext.swift | 7 +- .../Sources/SheetComponent.swift | 4 + .../Display/Source/TextAlertController.swift | 11 +- .../PremiumUI/Sources/PremiumDemoScreen.swift | 2 +- .../Sources/PremiumIntroScreen.swift | 10 +- submodules/TelegramApi/Sources/Api0.swift | 4 +- submodules/TelegramApi/Sources/Api15.swift | 18 +- submodules/TelegramApi/Sources/Api23.swift | 102 +- submodules/TelegramApi/Sources/Api24.swift | 166 ++-- submodules/TelegramApi/Sources/Api25.swift | 86 ++ .../ApiUtils/StoreMessage_Telegram.swift | 2 +- .../TextEntitiesMessageAttribute.swift | 2 +- .../State/AccountStateManagementUtils.swift | 7 + .../TelegramEngine/Payments/Stars.swift | 6 + ...hatMessageFactCheckBubbleContentNode.swift | 21 +- .../ChatMessageInvoiceBubbleContentNode.swift | 2 +- .../Sources/FactCheckAlertController.swift | 20 +- .../Sources/ListActionItemComponent.swift | 86 +- .../Sources/GiftAvatarComponent.swift | 10 +- .../AutomaticBusinessMessageSetupScreen.swift | 10 +- .../Sources/BusinessLinksSetupScreen.swift | 2 +- .../Sources/BusinessDaySetupScreen.swift | 2 +- .../Sources/BusinessRecipientListScreen.swift | 2 +- .../Sources/ChatbotSetupScreen.swift | 4 +- .../Stars/StarsPurchaseScreen/BUILD | 1 + .../Sources/ItemLoadingComponent.swift | 92 ++ .../Sources/StarsPurchaseScreen.swift | 248 ++--- .../Sources/StarsBalanceComponent.swift | 7 +- .../Sources/StarsTransactionScreen.swift | 885 ++++++++++++++++++ .../StarsTransactionsListPanelComponent.swift | 29 +- .../Sources/StarsTransactionsScreen.swift | 143 +-- .../Sources/StarsTransferScreen.swift | 112 ++- .../Sources/TextLoadingEffect.swift | 31 +- .../Stars/Android.imageset/Contents.json | 12 + .../Stars/Android.imageset/android.pdf | Bin 0 -> 1355 bytes .../Stars/Apple.imageset/Contents.json | 12 + .../Premium/Stars/Apple.imageset/apple.pdf | 159 ++++ .../Stars/Fragment.imageset/Contents.json | 12 + .../Stars/Fragment.imageset/fragment.pdf | Bin 0 -> 1283 bytes .../TelegramUI/Sources/ChatController.swift | 29 +- .../ChatInterfaceStateContextMenus.swift | 2 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 9 +- .../Sources/SharedAccountContext.swift | 12 +- .../WebUI/Sources/WebAppController.swift | 9 +- 45 files changed, 1927 insertions(+), 478 deletions(-) create mode 100644 submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift create mode 100644 submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/android.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/apple.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/fragment.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index cac42787f3..0d58780b9d 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12217,3 +12217,18 @@ Sorry for the inconvenience."; "Conversation.ContextMenuAddFactCheck" = "Add Fact Check"; "Conversation.ContextMenuEditFactCheck" = "Edit Fact Check"; + +"Stars.Purchase.GetStars" = "Get Stars"; +"Stars.Purchase.GetStarsInfo" = "Choose how many Stars you would like to buy."; + +"Stars.Purchase.Balance" = "Balance"; + +"Stars.Purchase.StarsNeeded_1" = "%@ Star Needed"; +"Stars.Purchase.StarsNeeded_any" = "%@ Stars Needed"; +"Stars.Purchase.StarsNeededInfo" = "Buy Stars to use them on **%@** and other miniapps."; + +"Stars.Purchase.Stars_1" = "%@ Star"; +"Stars.Purchase.Stars_any" = "%@ Stars"; +"Stars.Purchase.ShowMore" = "Show More Options"; +"Stars.Purchase.Info" = "By proceeding and purchasing Stars, you agree with [Terms and Conditions]()."; +"Stars.Purchase.Terms_URL" = "https://telegram.org/tos"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 67a5a95cef..dbdee4588f 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1042,9 +1042,10 @@ public protocol SharedAccountContext: AnyObject { func makeStoryStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: EnginePeer.Id, storyId: Int32, storyItem: EngineStoryItem, fromStory: Bool) -> ViewController func makeStarsTransactionsScreen(context: AccountContext, starsContext: StarsContext) -> ViewController - func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int32?) -> ViewController - func makeStarsTransferScreen(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController - + func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController + func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController + func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController + func makeDebugSettingsController(context: AccountContext?) -> ViewController? func navigateToCurrentCall() diff --git a/submodules/Components/SheetComponent/Sources/SheetComponent.swift b/submodules/Components/SheetComponent/Sources/SheetComponent.swift index 2626cccf9d..7e6678290e 100644 --- a/submodules/Components/SheetComponent/Sources/SheetComponent.swift +++ b/submodules/Components/SheetComponent/Sources/SheetComponent.swift @@ -62,6 +62,7 @@ public final class SheetComponent: Component { public let content: AnyComponent public let backgroundColor: BackgroundColor public let followContentSizeChanges: Bool + public let clipsContent: Bool public let externalState: ExternalState? public let animateOut: ActionSlot> @@ -69,12 +70,14 @@ public final class SheetComponent: Component { content: AnyComponent, backgroundColor: BackgroundColor, followContentSizeChanges: Bool = false, + clipsContent: Bool = false, externalState: ExternalState? = nil, animateOut: ActionSlot> ) { self.content = content self.backgroundColor = backgroundColor self.followContentSizeChanges = followContentSizeChanges + self.clipsContent = clipsContent self.externalState = externalState self.animateOut = animateOut } @@ -349,6 +352,7 @@ public final class SheetComponent: Component { if contentView.superview == nil { self.scrollView.addSubview(contentView) } + contentView.clipsToBounds = component.clipsContent if sheetEnvironment.isCentered { let y: CGFloat = floorToScreenPixels((availableSize.height - contentSize.height) / 2.0) transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentSize.width) / 2.0), y: -y), size: contentSize), completion: nil) diff --git a/submodules/Display/Source/TextAlertController.swift b/submodules/Display/Source/TextAlertController.swift index 54437bb0f6..252d5efc54 100644 --- a/submodules/Display/Source/TextAlertController.swift +++ b/submodules/Display/Source/TextAlertController.swift @@ -9,6 +9,7 @@ public enum TextAlertActionType { case genericAction case defaultAction case destructiveAction + case defaultDestructiveAction } public struct TextAlertAction { @@ -25,7 +26,11 @@ public struct TextAlertAction { public final class TextAlertContentActionNode: HighlightableButtonNode { private var theme: AlertControllerTheme - let action: TextAlertAction + public var action: TextAlertAction { + didSet { + self.updateTitle() + } + } private let backgroundNode: ASDisplayNode @@ -110,11 +115,11 @@ public final class TextAlertContentActionNode: HighlightableButtonNode { switch self.action.type { case .defaultAction, .genericAction: color = self.actionEnabled ? self.theme.accentColor : self.theme.disabledColor - case .destructiveAction: + case .destructiveAction, .defaultDestructiveAction: color = self.actionEnabled ? self.theme.destructiveColor : self.theme.disabledColor } switch self.action.type { - case .defaultAction: + case .defaultAction, .defaultDestructiveAction: font = Font.semibold(theme.baseFontSize) case .destructiveAction, .genericAction: break diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index df878b2a41..91671972a0 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -1107,7 +1107,7 @@ private final class DemoSheetContent: CombinedComponent { id: "background", component: AnyComponent( BlurredBackgroundComponent( - color: UIColor(rgb: 0x888888, alpha: 0.1) + color: UIColor(rgb: 0x888888, alpha: 0.1) ) ) ), diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 8871eff5c1..43b2b98045 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2018,7 +2018,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: gradientColors[i], foregroundColor: .white, iconName: perk.iconName - )))), + ))), false), action: { [weak state] _ in var demoSubject: PremiumDemoScreen.Subject switch perk { @@ -2185,7 +2185,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: gradientColors[min(i, gradientColors.count - 1)], foregroundColor: .white, iconName: perk.iconName - )))), + ))), false), action: { [weak state] _ in let isPremium = state?.isPremium == true if isPremium { @@ -2369,7 +2369,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: UIColor(rgb: 0x676bff), foregroundColor: .white, iconName: "Premium/BusinessPerk/Status" - )))), + ))), false), icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent( context: context.component.context, color: accentColor, @@ -2410,7 +2410,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: UIColor(rgb: 0x4492ff), foregroundColor: .white, iconName: "Premium/BusinessPerk/Tag" - )))), + ))), false), action: { _ in push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil)) } @@ -2441,7 +2441,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { backgroundColor: UIColor(rgb: 0x41a6a5), foregroundColor: .white, iconName: "Premium/Perk/Stories" - )))), + ))), false), action: { _ in push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false)) } diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 7bfe51dc5c..c008fca8c4 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -564,7 +564,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1205698681] = { return Api.MessageAction.parse_messageActionWebViewDataSentMe($0) } dict[546203849] = { return Api.MessageEntity.parse_inputMessageEntityMentionName($0) } dict[1981704948] = { return Api.MessageEntity.parse_messageEntityBankCard($0) } - dict[34469328] = { return Api.MessageEntity.parse_messageEntityBlockquote($0) } + dict[-238245204] = { return Api.MessageEntity.parse_messageEntityBlockquote($0) } dict[-1117713463] = { return Api.MessageEntity.parse_messageEntityBold($0) } dict[1827637959] = { return Api.MessageEntity.parse_messageEntityBotCommand($0) } dict[1280209983] = { return Api.MessageEntity.parse_messageEntityCashtag($0) } @@ -875,6 +875,8 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[-1269320843] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerAppStore($0) } dict[-382740222] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerFragment($0) } dict[2069236235] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerPlayMarket($0) } + dict[621656824] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerPremiumBot($0) } + dict[-1779253276] = { return Api.StarsTransactionPeer.parse_starsTransactionPeerUnsupported($0) } dict[-884757282] = { return Api.StatsAbsValueAndPrev.parse_statsAbsValueAndPrev($0) } dict[-1237848657] = { return Api.StatsDateRangeDays.parse_statsDateRangeDays($0) } dict[-1901828938] = { return Api.StatsGraph.parse_statsGraph($0) } diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index f82c179069..3924397ce3 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -2,7 +2,7 @@ public extension Api { indirect enum MessageEntity: TypeConstructorDescription { case inputMessageEntityMentionName(offset: Int32, length: Int32, userId: Api.InputUser) case messageEntityBankCard(offset: Int32, length: Int32) - case messageEntityBlockquote(offset: Int32, length: Int32) + case messageEntityBlockquote(flags: Int32, offset: Int32, length: Int32) case messageEntityBold(offset: Int32, length: Int32) case messageEntityBotCommand(offset: Int32, length: Int32) case messageEntityCashtag(offset: Int32, length: Int32) @@ -39,10 +39,11 @@ public extension Api { serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(length, buffer: buffer, boxed: false) break - case .messageEntityBlockquote(let offset, let length): + case .messageEntityBlockquote(let flags, let offset, let length): if boxed { - buffer.appendInt32(34469328) + buffer.appendInt32(-238245204) } + serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(offset, buffer: buffer, boxed: false) serializeInt32(length, buffer: buffer, boxed: false) break @@ -185,8 +186,8 @@ public extension Api { return ("inputMessageEntityMentionName", [("offset", offset as Any), ("length", length as Any), ("userId", userId as Any)]) case .messageEntityBankCard(let offset, let length): return ("messageEntityBankCard", [("offset", offset as Any), ("length", length as Any)]) - case .messageEntityBlockquote(let offset, let length): - return ("messageEntityBlockquote", [("offset", offset as Any), ("length", length as Any)]) + case .messageEntityBlockquote(let flags, let offset, let length): + return ("messageEntityBlockquote", [("flags", flags as Any), ("offset", offset as Any), ("length", length as Any)]) case .messageEntityBold(let offset, let length): return ("messageEntityBold", [("offset", offset as Any), ("length", length as Any)]) case .messageEntityBotCommand(let offset, let length): @@ -264,10 +265,13 @@ public extension Api { _1 = reader.readInt32() var _2: Int32? _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() let _c1 = _1 != nil let _c2 = _2 != nil - if _c1 && _c2 { - return Api.MessageEntity.messageEntityBlockquote(offset: _1!, length: _2!) + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.MessageEntity.messageEntityBlockquote(flags: _1!, offset: _2!, length: _3!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api23.swift b/submodules/TelegramApi/Sources/Api23.swift index 9f3e4a40be..a5b2dfc96c 100644 --- a/submodules/TelegramApi/Sources/Api23.swift +++ b/submodules/TelegramApi/Sources/Api23.swift @@ -710,6 +710,8 @@ public extension Api { case starsTransactionPeerAppStore case starsTransactionPeerFragment case starsTransactionPeerPlayMarket + case starsTransactionPeerPremiumBot + case starsTransactionPeerUnsupported public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { @@ -736,6 +738,18 @@ public extension Api { buffer.appendInt32(2069236235) } + break + case .starsTransactionPeerPremiumBot: + if boxed { + buffer.appendInt32(621656824) + } + + break + case .starsTransactionPeerUnsupported: + if boxed { + buffer.appendInt32(-1779253276) + } + break } } @@ -750,6 +764,10 @@ public extension Api { return ("starsTransactionPeerFragment", []) case .starsTransactionPeerPlayMarket: return ("starsTransactionPeerPlayMarket", []) + case .starsTransactionPeerPremiumBot: + return ("starsTransactionPeerPremiumBot", []) + case .starsTransactionPeerUnsupported: + return ("starsTransactionPeerUnsupported", []) } } @@ -775,85 +793,11 @@ public extension Api { public static func parse_starsTransactionPeerPlayMarket(_ reader: BufferReader) -> StarsTransactionPeer? { return Api.StarsTransactionPeer.starsTransactionPeerPlayMarket } - - } -} -public extension Api { - enum StatsAbsValueAndPrev: TypeConstructorDescription { - case statsAbsValueAndPrev(current: Double, previous: Double) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .statsAbsValueAndPrev(let current, let previous): - if boxed { - buffer.appendInt32(-884757282) - } - serializeDouble(current, buffer: buffer, boxed: false) - serializeDouble(previous, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .statsAbsValueAndPrev(let current, let previous): - return ("statsAbsValueAndPrev", [("current", current as Any), ("previous", previous as Any)]) - } - } - - public static func parse_statsAbsValueAndPrev(_ reader: BufferReader) -> StatsAbsValueAndPrev? { - var _1: Double? - _1 = reader.readDouble() - var _2: Double? - _2 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.StatsAbsValueAndPrev.statsAbsValueAndPrev(current: _1!, previous: _2!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum StatsDateRangeDays: TypeConstructorDescription { - case statsDateRangeDays(minDate: Int32, maxDate: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .statsDateRangeDays(let minDate, let maxDate): - if boxed { - buffer.appendInt32(-1237848657) - } - serializeInt32(minDate, buffer: buffer, boxed: false) - serializeInt32(maxDate, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .statsDateRangeDays(let minDate, let maxDate): - return ("statsDateRangeDays", [("minDate", minDate as Any), ("maxDate", maxDate as Any)]) - } - } - - public static func parse_statsDateRangeDays(_ reader: BufferReader) -> StatsDateRangeDays? { - 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.StatsDateRangeDays.statsDateRangeDays(minDate: _1!, maxDate: _2!) - } - else { - return nil - } + public static func parse_starsTransactionPeerPremiumBot(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerPremiumBot + } + public static func parse_starsTransactionPeerUnsupported(_ reader: BufferReader) -> StarsTransactionPeer? { + return Api.StarsTransactionPeer.starsTransactionPeerUnsupported } } diff --git a/submodules/TelegramApi/Sources/Api24.swift b/submodules/TelegramApi/Sources/Api24.swift index 51256e9e02..f42e53ce09 100644 --- a/submodules/TelegramApi/Sources/Api24.swift +++ b/submodules/TelegramApi/Sources/Api24.swift @@ -1,3 +1,83 @@ +public extension Api { + enum StatsAbsValueAndPrev: TypeConstructorDescription { + case statsAbsValueAndPrev(current: Double, previous: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .statsAbsValueAndPrev(let current, let previous): + if boxed { + buffer.appendInt32(-884757282) + } + serializeDouble(current, buffer: buffer, boxed: false) + serializeDouble(previous, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .statsAbsValueAndPrev(let current, let previous): + return ("statsAbsValueAndPrev", [("current", current as Any), ("previous", previous as Any)]) + } + } + + public static func parse_statsAbsValueAndPrev(_ reader: BufferReader) -> StatsAbsValueAndPrev? { + var _1: Double? + _1 = reader.readDouble() + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.StatsAbsValueAndPrev.statsAbsValueAndPrev(current: _1!, previous: _2!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum StatsDateRangeDays: TypeConstructorDescription { + case statsDateRangeDays(minDate: Int32, maxDate: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .statsDateRangeDays(let minDate, let maxDate): + if boxed { + buffer.appendInt32(-1237848657) + } + serializeInt32(minDate, buffer: buffer, boxed: false) + serializeInt32(maxDate, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .statsDateRangeDays(let minDate, let maxDate): + return ("statsDateRangeDays", [("minDate", minDate as Any), ("maxDate", maxDate as Any)]) + } + } + + public static func parse_statsDateRangeDays(_ reader: BufferReader) -> StatsDateRangeDays? { + 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.StatsDateRangeDays.statsDateRangeDays(minDate: _1!, maxDate: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum StatsGraph: TypeConstructorDescription { case statsGraph(flags: Int32, json: Api.DataJSON, zoomToken: String?) @@ -1340,89 +1420,3 @@ public extension Api { } } -public extension Api { - enum Timezone: TypeConstructorDescription { - case timezone(id: String, name: String, utcOffset: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .timezone(let id, let name, let utcOffset): - if boxed { - buffer.appendInt32(-7173643) - } - serializeString(id, buffer: buffer, boxed: false) - serializeString(name, buffer: buffer, boxed: false) - serializeInt32(utcOffset, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .timezone(let id, let name, let utcOffset): - return ("timezone", [("id", id as Any), ("name", name as Any), ("utcOffset", utcOffset as Any)]) - } - } - - public static func parse_timezone(_ reader: BufferReader) -> Timezone? { - var _1: String? - _1 = parseString(reader) - var _2: String? - _2 = parseString(reader) - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.Timezone.timezone(id: _1!, name: _2!, utcOffset: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum TopPeer: TypeConstructorDescription { - case topPeer(peer: Api.Peer, rating: Double) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .topPeer(let peer, let rating): - if boxed { - buffer.appendInt32(-305282981) - } - peer.serialize(buffer, true) - serializeDouble(rating, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .topPeer(let peer, let rating): - return ("topPeer", [("peer", peer as Any), ("rating", rating as Any)]) - } - } - - public static func parse_topPeer(_ reader: BufferReader) -> TopPeer? { - var _1: Api.Peer? - if let signature = reader.readInt32() { - _1 = Api.parse(reader, signature: signature) as? Api.Peer - } - var _2: Double? - _2 = reader.readDouble() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.TopPeer.topPeer(peer: _1!, rating: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api25.swift b/submodules/TelegramApi/Sources/Api25.swift index 283d8da677..ffad9c1ca7 100644 --- a/submodules/TelegramApi/Sources/Api25.swift +++ b/submodules/TelegramApi/Sources/Api25.swift @@ -1,3 +1,89 @@ +public extension Api { + enum Timezone: TypeConstructorDescription { + case timezone(id: String, name: String, utcOffset: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .timezone(let id, let name, let utcOffset): + if boxed { + buffer.appendInt32(-7173643) + } + serializeString(id, buffer: buffer, boxed: false) + serializeString(name, buffer: buffer, boxed: false) + serializeInt32(utcOffset, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .timezone(let id, let name, let utcOffset): + return ("timezone", [("id", id as Any), ("name", name as Any), ("utcOffset", utcOffset as Any)]) + } + } + + public static func parse_timezone(_ reader: BufferReader) -> Timezone? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.Timezone.timezone(id: _1!, name: _2!, utcOffset: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum TopPeer: TypeConstructorDescription { + case topPeer(peer: Api.Peer, rating: Double) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .topPeer(let peer, let rating): + if boxed { + buffer.appendInt32(-305282981) + } + peer.serialize(buffer, true) + serializeDouble(rating, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .topPeer(let peer, let rating): + return ("topPeer", [("peer", peer as Any), ("rating", rating as Any)]) + } + } + + public static func parse_topPeer(_ reader: BufferReader) -> TopPeer? { + var _1: Api.Peer? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Peer + } + var _2: Double? + _2 = reader.readDouble() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.TopPeer.topPeer(peer: _1!, rating: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum TopPeerCategory: TypeConstructorDescription { case topPeerCategoryBotsInline diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index 11cbedfba1..49d0d174f8 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -593,7 +593,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Underline)) case let .messageEntityStrike(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Strikethrough)) - case let .messageEntityBlockquote(offset, length): + case let .messageEntityBlockquote(_, offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BlockQuote)) case let .messageEntityBankCard(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BankCard)) diff --git a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift index 15e1f0d005..391c2d3bec 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TextEntitiesMessageAttribute.swift @@ -41,7 +41,7 @@ func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associa case .Strikethrough: apiEntities.append(.messageEntityStrike(offset: offset, length: length)) case .BlockQuote: - apiEntities.append(.messageEntityBlockquote(offset: offset, length: length)) + apiEntities.append(.messageEntityBlockquote(flags: 0, offset: offset, length: length)) case .Underline: apiEntities.append(.messageEntityUnderline(offset: offset, length: length)) case .BankCard: diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index be4b0354af..49b340a520 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -3872,6 +3872,13 @@ func replayFinalState( } } + if let previousFactCheckAttribute = previousMessage.attributes.first(where: { $0 is FactCheckMessageAttribute }) as? FactCheckMessageAttribute, let updatedFactCheckAttribute = message.attributes.first(where: { $0 is FactCheckMessageAttribute }) as? FactCheckMessageAttribute { + if case .Pending = updatedFactCheckAttribute.content, updatedFactCheckAttribute.hash == previousFactCheckAttribute.hash { + updatedAttributes.removeAll(where: { $0 is FactCheckMessageAttribute }) + updatedAttributes.append(previousFactCheckAttribute) + } + } + if let message = locallyRenderedMessage(message: message, peers: peers) { generatedEvent = reactionGeneratedEvent(previousMessage.reactionsAttribute, message.reactionsAttribute, message: message, transaction: transaction) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 7eb372692b..710a63d477 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -225,6 +225,10 @@ private extension StarsContext.State.Transaction { parsedPeer = .playMarket case .starsTransactionPeerFragment: parsedPeer = .fragment + case .starsTransactionPeerPremiumBot: + parsedPeer = .premiumBot + case .starsTransactionPeerUnsupported: + parsedPeer = .unsupported case let .starsTransactionPeer(apiPeer): guard let peer = transaction.getPeer(apiPeer.peerId) else { return nil @@ -243,6 +247,8 @@ public final class StarsContext { case appStore case playMarket case fragment + case premiumBot + case unsupported case peer(EnginePeer) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index 7bc7974568..c5b5b9efad 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -313,12 +313,10 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let titleBadgeString = NSAttributedString(string: item.presentationData.strings.Message_FactCheck_WhatIsThis, font: badgeFont, textColor: mainColor) let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayout(TextNodeLayoutArguments(attributedString: titleBadgeString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize)) - var finalAttributedText = attributedText - if "".isEmpty { - finalAttributedText = stringWithAppliedEntities(rawText + "\u{00A0}\u{00A0}\u{00A0}", entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil) - } + let finalAttributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil) as! NSMutableAttributedString + finalAttributedText.append(NSAttributedString(string: "__", font: textFont, textColor: .clear)) - let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: finalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor, customTruncationToken: NSAttributedString(string: "", font: textFont, textColor: .clear))) + let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: finalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor)) var canExpand = false var clippedTextHeight: CGFloat = textLayout.size.height @@ -336,6 +334,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode var titleFrameWithoutInsets = CGRect(origin: CGPoint(x: titleFrame.origin.x + textInsets.left, y: titleFrame.origin.y + textInsets.top), size: CGSize(width: titleFrame.width - textInsets.left - textInsets.right, height: titleFrame.height - textInsets.top - textInsets.bottom)) titleFrameWithoutInsets = titleFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) + let topInset: CGFloat = 5.0 let textSpacing: CGFloat = 3.0 let textFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: -textInsets.top + titleFrameWithoutInsets.height + textSpacing), size: textLayout.size) @@ -386,7 +385,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right) - boundingSize = CGSize(width: boundingWidth, height: titleFrameWithoutInsets.height + textFrameWithoutInsets.size.height + textSpacing) + boundingSize = CGSize(width: boundingWidth, height: topInset + titleFrameWithoutInsets.height + textFrameWithoutInsets.size.height + textSpacing) if let statusSizeAndApply = statusSizeAndApply { boundingSize.height += statusSizeAndApply.0.height } @@ -460,13 +459,13 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode } let _ = titleApply() - strongSelf.titleNode.frame = titleFrame + strongSelf.titleNode.frame = titleFrame.offsetBy(dx: 0.0, dy: topInset) let _ = titleBadgeApply() let _ = textApply() strongSelf.textNode.frame = CGRect(origin: .zero, size: textFrame.size) - var clippingTextFrame = textFrame + var clippingTextFrame = textFrame.offsetBy(dx: 0.0, dy: topInset) clippingTextFrame.size.height = clippedTextHeight - 3.0 if canExpand { @@ -523,7 +522,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode titleLineWidth = titleFrame.width } - let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.minX + titleLineWidth + titleBadgeSpacing + titleBadgePadding, y: floorToScreenPixels(titleFrame.midY - titleBadgeLayout.size.height / 2.0) - 1.0), size: titleBadgeLayout.size) + let titleBadgeFrame = CGRect(origin: CGPoint(x: titleFrame.minX + titleLineWidth + titleBadgeSpacing + titleBadgePadding, y: topInset + floorToScreenPixels(titleFrame.midY - titleBadgeLayout.size.height / 2.0) - 1.0), size: titleBadgeLayout.size) let badgeBackgroundFrame = titleBadgeFrame.insetBy(dx: -titleBadgePadding, dy: -1.0 + UIScreenPixel) strongSelf.titleBadgeLabel.frame = titleBadgeFrame @@ -560,7 +559,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode titleBadgeButton.setBackgroundImage(generateFilledCircleImage(diameter: badgeBackgroundFrame.height, color: mainColor.withMultipliedAlpha(0.1))?.stretchableImage(withLeftCapWidth: Int(badgeBackgroundFrame.height / 2), topCapHeight: Int(badgeBackgroundFrame.height / 2)), for: .normal) } - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.height + textSpacing)) + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top + topInset), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.height + textSpacing)) if isFirstTime { strongSelf.textClippingNode.frame = clippingTextFrame @@ -595,7 +594,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) } - let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) + let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: topInset + textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) if isFirstTime { strongSelf.statusNode.frame = statusFrame } else { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift index ab5480aea8..02855854c9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -62,7 +62,7 @@ public final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContent if let image = invoice.photo { automaticDownloadSettings = MediaAutoDownloadSettings.defaultSettings mediaAndFlags = ([image], [.preferMediaBeforeText]) - } else { + } else if invoice.currency != "XTR" { let invoiceLabel = item.presentationData.strings.Message_InvoiceLabel var invoiceText = "\(formatCurrencyAmount(invoice.totalAmount, currency: invoice.currency)) " invoiceText += invoiceLabel diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift index 758560fd94..023ad26a55 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift @@ -20,6 +20,7 @@ private final class FactCheckAlertContentNode: AlertContentNode { private var presentationTheme: PresentationTheme private let strings: PresentationStrings private let text: String + private let initialValue: String private let titleView = ComponentView() @@ -58,6 +59,7 @@ private final class FactCheckAlertContentNode: AlertContentNode { self.presentationTheme = presentationTheme self.strings = strings self.text = text + self.initialValue = value if !value.isEmpty { self.inputFieldExternalState.initialText = chatInputStateStringWithAppliedEntities(value, entities: entities) @@ -271,7 +273,23 @@ private final class FactCheckAlertContentNode: AlertContentNode { } if let lastActionNode = self.actionNodes.last { - lastActionNode.actionEnabled = self.inputFieldExternalState.hasText + if self.initialValue.isEmpty { + lastActionNode.actionEnabled = self.inputFieldExternalState.hasText + } else { + if self.inputFieldExternalState.hasText { + lastActionNode.action = TextAlertAction( + type: .defaultAction, + title: self.strings.Common_Done, + action: lastActionNode.action.action + ) + } else { + lastActionNode.action = TextAlertAction( + type: .defaultDestructiveAction, + title: self.strings.FactCheck_Remove, + action: lastActionNode.action.action + ) + } + } } let resultSize = CGSize(width: resultWidth, height: titleSize.height + spacing + inputFieldSize.height + 17.0 + actionsHeight + insets.top + insets.bottom) diff --git a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift index 2e45b46a04..12ca89591c 100644 --- a/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift +++ b/submodules/TelegramUI/Components/ListActionItemComponent/Sources/ListActionItemComponent.swift @@ -105,7 +105,7 @@ public final class ListActionItemComponent: Component { } case check(Check) - case custom(AnyComponentWithIdentity) + case custom(AnyComponentWithIdentity, Bool) } public enum Highlighting { @@ -119,6 +119,7 @@ public final class ListActionItemComponent: Component { } public let theme: PresentationTheme + public let background: AnyComponent? public let title: AnyComponent public let titleAlignment: Alignment public let contentInsets: UIEdgeInsets @@ -127,9 +128,11 @@ public final class ListActionItemComponent: Component { public let accessory: Accessory? public let action: ((UIView) -> Void)? public let highlighting: Highlighting + public let updateIsHighlighted: ((UIView, Bool) -> Void)? public init( theme: PresentationTheme, + background: AnyComponent? = nil, title: AnyComponent, titleAlignment: Alignment = .default, contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0), @@ -137,9 +140,11 @@ public final class ListActionItemComponent: Component { icon: Icon? = nil, accessory: Accessory? = .arrow, action: ((UIView) -> Void)?, - highlighting: Highlighting = .default + highlighting: Highlighting = .default, + updateIsHighlighted: ((UIView, Bool) -> Void)? = nil ) { self.theme = theme + self.background = background self.title = title self.titleAlignment = titleAlignment self.contentInsets = contentInsets @@ -148,12 +153,16 @@ public final class ListActionItemComponent: Component { self.accessory = accessory self.action = action self.highlighting = highlighting + self.updateIsHighlighted = updateIsHighlighted } public static func ==(lhs: ListActionItemComponent, rhs: ListActionItemComponent) -> Bool { if lhs.theme !== rhs.theme { return false } + if lhs.background != rhs.background { + return false + } if lhs.title != rhs.title { return false } @@ -255,6 +264,7 @@ public final class ListActionItemComponent: Component { } public final class View: HighlightTrackingButton, ListSectionComponent.ChildView { + private var background: ComponentView? private let title = ComponentView() private var leftIcon: ComponentView? private var leftCheckView: CheckView? @@ -287,6 +297,7 @@ public final class ListActionItemComponent: Component { guard let self, let component = self.component, component.action != nil else { return } + component.updateIsHighlighted?(self, isHighlighted) if isHighlighted, component.highlighting == .disabled { return } @@ -389,14 +400,8 @@ public final class ListActionItemComponent: Component { contentLeftInset = floor((availableSize.width - titleSize.width) / 2.0) } - let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: contentHeight), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - titleView.isUserInteractionEnabled = false - self.addSubview(titleView) - } - transition.setFrame(view: titleView, frame: titleFrame) - } + + let titleY = contentHeight contentHeight += titleSize.height contentHeight += component.contentInsets.bottom @@ -500,9 +505,9 @@ public final class ListActionItemComponent: Component { transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size)) leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: transition) } - case let .custom(customLeftIcon): + case let .custom(customLeftIcon, adjustLeftInset): var resetLeftIcon = false - if case let .custom(previousCustomLeftIcon) = previousComponent?.leftIcon { + if case let .custom(previousCustomLeftIcon, _) = previousComponent?.leftIcon { if previousCustomLeftIcon.id != customLeftIcon.id { resetLeftIcon = true } @@ -542,7 +547,13 @@ public final class ListActionItemComponent: Component { environment: {}, containerSize: CGSize(width: availableSize.width, height: availableSize.height) ) - let leftIconFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - leftIconSize.width) * 0.5), y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize) + let leftIconX: CGFloat + if adjustLeftInset { + leftIconX = 15.0 + } else { + leftIconX = floor((contentLeftInset - leftIconSize.width) * 0.5) + } + let leftIconFrame = CGRect(origin: CGPoint(x: leftIconX, y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize) if let leftIconView = leftIcon.view { if leftIconView.superview == nil { leftIconView.isUserInteractionEnabled = false @@ -551,6 +562,9 @@ public final class ListActionItemComponent: Component { } leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame) } + if adjustLeftInset { + contentLeftInset = 22.0 + leftIconSize.width + } } } else { if let leftIcon = self.leftIcon { @@ -738,8 +752,54 @@ public final class ListActionItemComponent: Component { } } + let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: titleY), size: titleSize) + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + self.separatorInset = contentLeftInset + if let backgroundComponent = component.background { + var backgroundTransition = transition + let background: ComponentView + if let current = self.background { + background = current + } else { + backgroundTransition = backgroundTransition.withAnimation(.none) + background = ComponentView() + self.background = background + } + + let backgroundSize = background.update( + transition: backgroundTransition, + component: backgroundComponent, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: contentHeight) + ) + let backgroundFrame = CGRect(origin: .zero, size: backgroundSize) + if let backgroundView = background.view { + if backgroundView.superview == nil { + backgroundView.isUserInteractionEnabled = false + self.addSubview(backgroundView) + transition.animateAlpha(view: backgroundView, from: 0.0, to: 1.0) + } + backgroundTransition.setFrame(view: backgroundView, frame: backgroundFrame) + } + } else { + if let background = self.background { + self.background = nil + if let backgroundView = background.view { + transition.setAlpha(view: backgroundView, alpha: 0.0, completion: { [weak backgroundView] _ in + backgroundView?.removeFromSuperview() + }) + } + } + } + return CGSize(width: availableSize.width, height: contentHeight) } } diff --git a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift index 60e8191424..2890dcbd85 100644 --- a/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift +++ b/submodules/TelegramUI/Components/Premium/PremiumStarComponent/Sources/GiftAvatarComponent.swift @@ -20,25 +20,29 @@ public final class GiftAvatarComponent: Component { let context: AccountContext let theme: PresentationTheme let peers: [EnginePeer] + let photo: TelegramMediaWebFile? let isVisible: Bool let hasIdleAnimations: Bool let hasScaleAnimation: Bool + let avatarSize: CGFloat let color: UIColor? let offset: CGFloat? - public init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], isVisible: Bool, hasIdleAnimations: Bool, hasScaleAnimation: Bool = true, color: UIColor? = nil, offset: CGFloat? = nil) { + public init(context: AccountContext, theme: PresentationTheme, peers: [EnginePeer], photo: TelegramMediaWebFile? = nil, isVisible: Bool, hasIdleAnimations: Bool, hasScaleAnimation: Bool = true, avatarSize: CGFloat = 100.0, color: UIColor? = nil, offset: CGFloat? = nil) { self.context = context self.theme = theme self.peers = peers + self.photo = photo self.isVisible = isVisible self.hasIdleAnimations = hasIdleAnimations self.hasScaleAnimation = hasScaleAnimation + self.avatarSize = avatarSize self.color = color self.offset = offset } public static func ==(lhs: GiftAvatarComponent, rhs: GiftAvatarComponent) -> Bool { - return lhs.peers == rhs.peers && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.offset == rhs.offset + return lhs.peers == rhs.peers && lhs.photo == rhs.photo && lhs.theme === rhs.theme && lhs.isVisible == rhs.isVisible && lhs.hasIdleAnimations == rhs.hasIdleAnimations && lhs.hasScaleAnimation == rhs.hasScaleAnimation && lhs.avatarSize == rhs.avatarSize && lhs.offset == rhs.offset } public final class View: UIView, SCNSceneRendererDelegate, ComponentTaggedView { @@ -316,7 +320,7 @@ public final class GiftAvatarComponent: Component { self.mergedAvatarsNode = nil self.avatarNode.isHidden = false - let avatarSize = CGSize(width: 100.0, height: 100.0) + let avatarSize = CGSize(width: component.avatarSize, height: component.avatarSize) if let peer = component.peers.first { self.avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: avatarSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index 2db55b4b3b..44d712798c 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -856,7 +856,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/ComposeIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -933,7 +933,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { image: checkIcon, tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1203,7 +1203,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { image: checkIcon, tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1233,7 +1233,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { image: checkIcon, tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -1280,7 +1280,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/AddIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift index 83f55acc75..86ea07efa8 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinksSetupScreen.swift @@ -416,7 +416,7 @@ final class BusinessLinksSetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Item List/AddLinkIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift index c258c60fb4..35689013c9 100644 --- a/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/BusinessHoursSetupScreen/Sources/BusinessDaySetupScreen.swift @@ -531,7 +531,7 @@ final class BusinessDaySetupScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Item List/AddTimeIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift index cd44260599..7f82689d75 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift @@ -425,7 +425,7 @@ final class BusinessRecipientListScreenComponent: Component { leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( name: "Chat List/AddIcon", tintColor: environment.theme.list.itemAccentColor - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 7709c95048..4571ab2149 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -774,7 +774,7 @@ final class ChatbotSetupScreenComponent: Component { image: checkIcon, tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { @@ -804,7 +804,7 @@ final class ChatbotSetupScreenComponent: Component { image: checkIcon, tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor, contentMode: .center - )))), + ))), false), accessory: nil, action: { [weak self] _ in guard let self else { diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD index 91f04b157a..dd5b8cbbb6 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/BUILD @@ -32,6 +32,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/TelegramUI/Components/ListActionItemComponent", "//submodules/TelegramUI/Components/ScrollComponent", + "//submodules/TelegramUI/Components/TextLoadingEffect", "//submodules/TelegramUI/Components/Premium/PremiumStarComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/BundleIconComponent", diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift new file mode 100644 index 0000000000..24905d0bf9 --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/ItemLoadingComponent.swift @@ -0,0 +1,92 @@ +import Foundation +import UIKit +import Display +import AppBundle +import HierarchyTrackingLayer +import ComponentFlow +import TextLoadingEffect + +final class ItemLoadingComponent: Component { + private let color: UIColor + + public init(color: UIColor) { + self.color = color + } + + public static func ==(lhs: ItemLoadingComponent, rhs: ItemLoadingComponent) -> Bool { + if !lhs.color.isEqual(rhs.color) { + return false + } + return true + } + + public final class View: UIView { + private let loadingView = TextLoadingEffectView() + private let borderView = UIImageView() + + private let borderMaskView = UIView() + private let borderMaskGradientView = UIImageView() + private let borderMaskFillView = UIImageView() + + private var component: ItemLoadingComponent? + + override public init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.loadingView) + self.addSubview(self.borderView) + + self.borderView.image = generateFilledRoundedRectImage(size: CGSize(width: 24.0, height: 24.0), cornerRadius: 10.0, color: nil, strokeColor: .white, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10).withRenderingMode(.alwaysTemplate) + + self.borderMaskView.backgroundColor = .clear + self.borderMaskFillView.backgroundColor = .white + + self.borderMaskView.addSubview(self.borderMaskFillView) + self.borderMaskFillView.addSubview(self.borderMaskGradientView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func playAppearanceAnimation() { + self.borderView.mask = self.borderMaskView + + let gradientWidth = self.borderView.bounds.width * 0.4 + self.borderMaskGradientView.image = generateGradientImage(size: CGSize(width: gradientWidth, height: 24.0), colors: [UIColor.white, UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 1.0], direction: .horizontal) + + self.borderMaskGradientView.frame = CGRect(origin: CGPoint(x: self.borderView.bounds.width, y: 0.0), size: CGSize(width: gradientWidth, height: self.borderView.bounds.height)) + self.borderMaskFillView.frame = CGRect(origin: .zero, size: self.borderView.bounds.size) + + self.borderMaskFillView.layer.animatePosition(from: CGPoint(x: -self.borderView.bounds.width, y: 0.0), to: .zero, duration: 1.0, removeOnCompletion: false, additive: true, completion: { _ in + self.borderView.mask = nil + }) + } + + func update(component: ItemLoadingComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let isFirstTime = self.component == nil + + self.component = component + + self.borderView.tintColor = component.color + + self.loadingView.update(color: component.color, rect: CGRect(origin: .zero, size: availableSize)) + transition.setFrame(view: self.borderView, frame: CGRect(origin: .zero, size: availableSize)) + self.borderMaskView.frame = self.borderView.bounds + + if isFirstTime { + self.playAppearanceAnimation() + } + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift index a4791b830e..ecddb0f63b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsPurchaseScreen/Sources/StarsPurchaseScreen.swift @@ -45,7 +45,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let context: AccountContext let options: [StarsTopUpOption] let peerId: EnginePeer.Id? - let requiredStars: Int32? + let requiredStars: Int64? + let selectedProductId: String? let forceDark: Bool let products: [StarsProduct]? let expanded: Bool @@ -56,7 +57,8 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { context: AccountContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, - requiredStars: Int32?, + requiredStars: Int64?, + selectedProductId: String?, forceDark: Bool, products: [StarsProduct]?, expanded: Bool, @@ -67,6 +69,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { self.options = options self.peerId = peerId self.requiredStars = requiredStars + self.selectedProductId = selectedProductId self.forceDark = forceDark self.products = products self.expanded = expanded @@ -87,6 +90,9 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { if lhs.requiredStars != rhs.requiredStars { return false } + if lhs.selectedProductId != rhs.selectedProductId { + return false + } if lhs.forceDark != rhs.forceDark { return false } @@ -157,6 +163,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { state.products = context.component.products let theme = environment.theme + let strings = environment.strings let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 } let availableWidth = context.availableSize.width @@ -202,13 +209,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { let textFont = Font.regular(15.0) let boldTextFont = Font.semibold(15.0) - //TODO:localize let textString: String -// if let peer = state.peer, let requiredStars = context.component.requiredStars { -// textString = "\(peer.compactDisplayTitle) requests \(requiredStars) Stars.\n\nAvailable balance: **1000 Stars**.\n\nBuy **Stars** to unlock **content and services** in miniapps on **Telegram**." -// } else { - textString = "Choose how many Stars you would like to buy." -// } + if let _ = context.component.requiredStars { + textString = strings.Stars_Purchase_StarsNeededInfo(state.peer?.compactDisplayTitle ?? "").string + } else { + textString = strings.Stars_Purchase_GetStarsInfo + } let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: accentColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) @@ -272,21 +278,23 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { ] let externalStateUpdated = context.component.stateUpdated - let layoutPerks = { - size.height += 8.0 - - var i = 0 - var items: [AnyComponentWithIdentity] = [] - - guard let products = state.products else { - return - } + + size.height += 8.0 + + var i = 0 + var items: [AnyComponentWithIdentity] = [] + + if let products = state.products { for product in products { + if let requiredStars = context.component.requiredStars, requiredStars > product.option.count { + continue + } + if !context.component.expanded && !initialValues.contains(product.option.count) { continue } - - let title = "\(product.option.count) Stars" + + let title = strings.Stars_Purchase_Stars(Int32(product.option.count)) let price = product.price let titleComponent = AnyComponent(MultilineTextComponent( @@ -298,6 +306,15 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { maximumNumberOfLines: 0 )) + let backgroundComponent: AnyComponent? + if product.storeProduct.id == context.component.selectedProductId { + backgroundComponent = AnyComponent( + ItemLoadingComponent(color: environment.theme.list.itemAccentColor) + ) + } else { + backgroundComponent = nil + } + let buy = context.component.buy items.append(AnyComponentWithIdentity( id: product.id, @@ -307,11 +324,12 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { footer: nil, items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, + background: backgroundComponent, title: titleComponent, contentInsets: UIEdgeInsets(top: 12.0, left: -6.0, bottom: 12.0, right: 0.0), leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(StarsIconComponent( count: stars[product.option.count] ?? 1 - )))), + ))), true), accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: price, @@ -322,66 +340,71 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), action: { _ in buy(product) + }, + highlighting: .disabled, + updateIsHighlighted: { view, isHighlighted in + let transition: Transition = .easeInOut(duration: 0.25) + if let superview = view.superview { + transition.setScale(view: superview, scale: isHighlighted ? 0.9 : 1.0) + } } )))] )) )) i += 1 } - - if !context.component.expanded { - let titleComponent = AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: "Show More Options", - font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemAccentColor - )), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )) - - let titleCombinedComponent = AnyComponent(HStack([ - AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent), - AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BundleIconComponent(name: "Chat/Input/Search/DownButton", tintColor: environment.theme.list.itemAccentColor))) - ], spacing: 1.0)) - - items.append(AnyComponentWithIdentity( - id: items.count, - component: AnyComponent(ListSectionComponent( - theme: environment.theme, - header: nil, - footer: nil, - items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( - theme: environment.theme, - title: titleCombinedComponent, - titleAlignment: .center, - contentInsets: UIEdgeInsets(top: 7.0, left: 0.0, bottom: 7.0, right: 0.0), - leftIcon: nil, - accessory: .none, - action: { _ in - externalStateUpdated(.easeInOut(duration: 0.3)) - } - )))] - )) - )) - } - - let list = list.update( - component: VStack(items, spacing: 16.0), - environment: {}, - availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), - transition: context.transition - ) - context.add(list - .position(CGPoint(x: availableWidth / 2.0, y: size.height + list.size.height / 2.0)) - ) - size.height += list.size.height - - size.height += 23.0 } - layoutPerks() + if !context.component.expanded { + let titleComponent = AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: strings.Stars_Purchase_ShowMore, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )) + + let titleCombinedComponent = AnyComponent(HStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: titleComponent), + AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(BundleIconComponent(name: "Chat/Input/Search/DownButton", tintColor: environment.theme.list.itemAccentColor))) + ], spacing: 1.0)) + + items.append(AnyComponentWithIdentity( + id: items.count, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: titleCombinedComponent, + titleAlignment: .center, + contentInsets: UIEdgeInsets(top: 7.0, left: 0.0, bottom: 7.0, right: 0.0), + leftIcon: nil, + accessory: .none, + action: { _ in + externalStateUpdated(.easeInOut(duration: 0.3)) + } + )))] + )) + )) + } + + let list = list.update( + component: VStack(items, spacing: 16.0), + environment: {}, + availableSize: CGSize(width: availableWidth - sideInsets, height: .greatestFiniteMagnitude), + transition: context.transition + ) + context.add(list + .position(CGPoint(x: availableWidth / 2.0, y: size.height + list.size.height / 2.0)) + ) + size.height += list.size.height + size.height += 23.0 + let termsFont = Font.regular(13.0) let termsTextColor = environment.theme.list.freeTextColor @@ -390,9 +413,10 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { }) let textSideInset: CGFloat = 16.0 + let component = context.component let termsText = termsText.update( component: BalancedTextComponent( - text: .markdown(text: "By proceeding and purchasing Stars, you agree with [Terms and Conditions]().", attributes: termsMarkdownAttributes), + text: .markdown(text: strings.Stars_Purchase_Info, attributes: termsMarkdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2, @@ -405,7 +429,7 @@ private final class StarsPurchaseScreenContentComponent: CombinedComponent { } }, tapAction: { attributes, _ in - + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Purchase_Terms_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } ), environment: {}, @@ -432,22 +456,22 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { let starsContext: StarsContext let options: [StarsTopUpOption] let peerId: EnginePeer.Id? - let requiredStars: Int32? + let requiredStars: Int64? let forceDark: Bool let updateInProgress: (Bool) -> Void let present: (ViewController) -> Void - let completion: () -> Void + let completion: (Int64) -> Void init( context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, - requiredStars: Int32?, + requiredStars: Int64?, forceDark: Bool, updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, - completion: @escaping () -> Void + completion: @escaping (Int64) -> Void ) { self.context = context self.starsContext = starsContext @@ -486,14 +510,14 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { private let context: AccountContext private let updateInProgress: (Bool) -> Void private let present: (ViewController) -> Void - private let completion: () -> Void + private let completion: (Int64) -> Void var topContentOffset: CGFloat? var bottomContentOffset: CGFloat? var hasIdleAnimations = true - var inProgress = false + var progressProduct: StarsProduct? private(set) var promoConfiguration: PremiumPromoConfiguration? @@ -514,7 +538,7 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { initialOptions: [StarsTopUpOption], updateInProgress: @escaping (Bool) -> Void, present: @escaping (ViewController) -> Void, - completion: @escaping () -> Void + completion: @escaping (Int64) -> Void ) { self.context = context self.updateInProgress = updateInProgress @@ -569,13 +593,13 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } func buy(product: StarsProduct) { - guard let inAppPurchaseManager = self.context.inAppPurchaseManager, !self.inProgress else { + guard let inAppPurchaseManager = self.context.inAppPurchaseManager, self.progressProduct == nil else { return } - self.inProgress = true + self.progressProduct = product self.updateInProgress(true) - self.updated(transition: .immediate) + self.updated(transition: .easeInOut(duration: 0.2)) let (currency, amount) = product.storeProduct.priceCurrencyAndAmount let purpose: AppStoreTransactionPurpose = .stars(count: product.option.count, currency: currency, amount: amount) @@ -588,17 +612,16 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(product.storeProduct, purpose: purpose) |> deliverOnMainQueue).start(next: { [weak self] status in if let self, case .purchased = status { - self.inProgress = false self.updateInProgress(false) - self.updated(transition: .easeInOut(duration: 0.25)) - self.completion() + self.updated(transition: .easeInOut(duration: 0.2)) + self.completion(product.option.count) } }, error: { [weak self] error in if let strongSelf = self { - strongSelf.inProgress = false + strongSelf.progressProduct = nil strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) + strongSelf.updated(transition: .easeInOut(duration: 0.2)) var errorText: String? switch error { @@ -623,9 +646,9 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { } })) } else { - strongSelf.inProgress = false + strongSelf.progressProduct = nil strongSelf.updateInProgress(false) - strongSelf.updated(transition: .immediate) + strongSelf.updated(transition: .easeInOut(duration: 0.2)) } } }) @@ -657,6 +680,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { return { context in let environment = context.environment[EnvironmentType.self].value let state = context.state + + let strings = environment.strings let background = background.update(component: Rectangle(color: environment.theme.list.blocksBackgroundColor), environment: {}, availableSize: context.availableSize, transition: context.transition) @@ -697,10 +722,16 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { transition: context.transition ) - //TODO:localize + let titleText: String + if let requiredStars = context.component.requiredStars { + titleText = strings.Stars_Purchase_StarsNeeded(Int32(requiredStars)) + } else { + titleText = strings.Stars_Purchase_GetStars + } + let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: "Get Stars", font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: titleText, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -722,11 +753,11 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903))!, environment.theme) } - let balanceAttributedString = parseMarkdownIntoAttributedString("Balance: * **\(state.starsState?.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString - if let range = balanceAttributedString.string.range(of: "*"), let chevronImage = state.cachedChevronImage?.0 { + let balanceAttributedString = parseMarkdownIntoAttributedString(" \(strings.Stars_Purchase_Balance)\n # **\(state.starsState?.balance ?? 0)**", attributes: markdownAttributes, textAlignment: .right).mutableCopy() as! NSMutableAttributedString + if let range = balanceAttributedString.string.range(of: "#"), let chevronImage = state.cachedChevronImage?.0 { balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string)) - balanceAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: balanceAttributedString.string)) + balanceAttributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: balanceAttributedString.string)) } let balanceText = balanceText.update( component: MultilineTextComponent( @@ -745,11 +776,12 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { options: context.component.options, peerId: context.component.peerId, requiredStars: context.component.requiredStars, + selectedProductId: state.progressProduct?.storeProduct.id, forceDark: context.component.forceDark, products: state.products, expanded: state.isExpanded, stateUpdated: { [weak state] transition in - scrollAction.invoke(CGPoint(x: 0.0, y: 176.0)) + scrollAction.invoke(CGPoint(x: 0.0, y: 170.0)) state?.isExpanded = true state?.updated(transition: transition) }, @@ -768,8 +800,8 @@ private final class StarsPurchaseScreenComponent: CombinedComponent { contentOffsetWillCommit: { targetContentOffset in if targetContentOffset.pointee.y < 100.0 { targetContentOffset.pointee = CGPoint(x: 0.0, y: 0.0) - } else if targetContentOffset.pointee.y < 176.0 { - targetContentOffset.pointee = CGPoint(x: 0.0, y: 176.0) + } else if targetContentOffset.pointee.y < 170.0 { + targetContentOffset.pointee = CGPoint(x: 0.0, y: 170.0) } }, resetScroll: scrollAction @@ -855,9 +887,10 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, - requiredStars: Int32?, + requiredStars: Int64?, modal: Bool = true, - forceDark: Bool = false + forceDark: Bool = false, + completion: @escaping (Int64) -> Void = { _ in } ) { self.context = context self.starsContext = starsContext @@ -865,7 +898,7 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { var updateInProgressImpl: ((Bool) -> Void)? var presentImpl: ((ViewController) -> Void)? - var completionImpl: (() -> Void)? + var completionImpl: ((Int64) -> Void)? super.init(context: context, component: StarsPurchaseScreenComponent( context: context, starsContext: starsContext, @@ -879,8 +912,8 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { present: { c in presentImpl?(c) }, - completion: { - completionImpl?() + completion: { stars in + completionImpl?(stars) } ), navigationBarAppearance: .transparent, presentationMode: modal ? .modal : .default, theme: forceDark ? .dark : .default) @@ -906,9 +939,11 @@ public final class StarsPurchaseScreen: ViewControllerComponentContainer { self.present(c, in: .window(.root)) } } - completionImpl = { [weak self] in + completionImpl = { [weak self] stars in if let self { self.animateSuccess() + + completion(stars) } } } @@ -996,12 +1031,13 @@ func generateStarsIcon(count: Int) -> UIImage { var originX = floorToScreenPixels((size.width - totalWidth) / 2.0) - if let cgImage = image.cgImage, let partCGImage = partImage.cgImage { + let mainImage = UIImage(bundleImageName: "Premium/Stars/Star") + if let cgImage = mainImage?.cgImage, let partCGImage = partImage.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: originX, y: 0.0), size: imageSize), byTiling: false) originX += spacing for _ in 0 ..< count - 1 { - context.draw(partCGImage, in: CGRect(origin: CGPoint(x: originX, y: 0.0), size: imageSize), byTiling: false) + context.draw(partCGImage, in: CGRect(origin: CGPoint(x: originX, y: UIScreenPixel), size: imageSize), byTiling: false) originX += spacing } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift index 28ac40019e..e1371b6504 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsBalanceComponent.swift @@ -62,6 +62,7 @@ final class StarsBalanceComponent: Component { } func update(component: StarsBalanceComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let isFirstTime = self.component == nil self.component = component let sideInset: CGFloat = 16.0 @@ -76,17 +77,13 @@ final class StarsBalanceComponent: Component { )) let titleSize = self.title.update( - transition: .easeInOut(duration: 0.2), + transition: isFirstTime ? .immediate : .easeInOut(duration: 0.2), component: AnyComponent( AnimatedTextComponent( font: Font.with(size: 48.0, design: .round, weight: .semibold), color: component.theme.list.itemPrimaryTextColor, items: animatedTextItems ) -// MultilineTextComponent( -// text: .plain(NSAttributedString(string: "\(component.count)", font: Font.with(size: 48.0, design: .round, weight: .semibold), textColor: component.theme.list.itemPrimaryTextColor)), -// horizontalAlignment: .center -// ) ), environment: {}, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift new file mode 100644 index 0000000000..19f371a0bb --- /dev/null +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift @@ -0,0 +1,885 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import TelegramCore +import SwiftSignalKit +import AccountContext +import TelegramPresentationData +import PresentationDataUtils +import ComponentFlow +import ViewControllerComponent +import SheetComponent +import MultilineTextComponent +import BundleIconComponent +import SolidRoundedButtonComponent +import Markdown +import BalancedTextComponent +import AvatarNode +import TextFormat +import TelegramStringFormatting +import UndoUI +import PremiumStarComponent + +private final class StarsTransactionSheetContent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: StarsTransactionScreen.Subject + let action: () -> Void + let cancel: (Bool) -> Void + let openPeer: (EnginePeer) -> Void + + init( + context: AccountContext, + subject: StarsTransactionScreen.Subject, + action: @escaping () -> Void, + cancel: @escaping (Bool) -> Void, + openPeer: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.subject = subject + self.action = action + self.cancel = cancel + self.openPeer = openPeer + } + + static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + private var disposable: Disposable? + var initialized = false + + var peerMap: [EnginePeer.Id: EnginePeer] = [:] + + var cachedCloseImage: (UIImage, PresentationTheme)? + + var inProgress = false + + init(context: AccountContext, subject: StarsTransactionScreen.Subject) { + self.context = context + + super.init() + + var peerIds: [EnginePeer.Id] = [] + switch subject { + case let .transaction(transaction): + if case let .peer(peer) = transaction.peer { + peerIds.append(peer.id) + } + } + + self.disposable = (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 in peerIds { + if let maybePeer = peers[peerId], let peer = maybePeer { + peersMap[peerId] = peer + } + } + strongSelf.peerMap = peersMap + strongSelf.initialized = true + + strongSelf.updated(transition: .immediate) + } + }) + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context, subject: self.subject) + } + + static var body: Body { + let closeButton = Child(Button.self) + let title = Child(MultilineTextComponent.self) + let star = Child(GiftAvatarComponent.self) + let description = Child(BalancedTextComponent.self) + let table = Child(TableComponent.self) + let additional = Child(BalancedTextComponent.self) + let button = Child(SolidRoundedButtonComponent.self) + + return { context in + let environment = context.environment[ViewControllerComponentContainer.Environment.self].value + let component = context.component + let theme = environment.theme + let strings = environment.strings + let dateTimeFormat = environment.dateTimeFormat + let accountContext = context.component.context + + let state = context.state + let subject = component.subject + + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let textSideInset: CGFloat = 32.0 + environment.safeInsets.left + + let closeImage: UIImage + if let (image, theme) = state.cachedCloseImage, theme === environment.theme { + 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: { [weak component] in + component?.cancel(true) + } + ), + availableSize: CGSize(width: 30.0, height: 30.0), + transition: .immediate + ) + + let titleText: String + let descriptionText: String + let additionalText: String + let buttonText: String + + let transactionId: String + let date: Int32 + let toPeer: EnginePeer? + + let gloss = false + switch subject { + case let .transaction(transaction): + titleText = "Product Title" + if transaction.count < 0 { + descriptionText = "- \(transaction.count * -1) ⭐️" + } else { + descriptionText = "+ \(transaction.count) ⭐️" + } + additionalText = "You can dispute this transaction [here]()." + buttonText = "OK" + + transactionId = transaction.id + date = transaction.date + if case let .peer(peer) = transaction.peer { + toPeer = peer + } else { + toPeer = nil + } + } + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: titleText, + font: Font.semibold(24.0), + textColor: theme.actionSheet.primaryTextColor, + paragraphAlignment: .center + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0 - 60.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let star = star.update( + component: GiftAvatarComponent( + context: component.context, + theme: theme, + peers: toPeer.flatMap { [$0] } ?? [], + isVisible: true, + hasIdleAnimations: true, + hasScaleAnimation: false, + avatarSize: 90.0, + color: UIColor(rgb: 0xf7ab04) + ), +// PremiumStarComponent(isIntro: false, isVisible: true, hasIdleAnimations: true), + availableSize: CGSize(width: context.availableSize.width, height: 200.0), + transition: .immediate + ) + + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.primaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) + let description = description.update( + component: BalancedTextComponent( + text: .plain(NSAttributedString(string: descriptionText, font: boldTextFont, textColor: descriptionText.hasPrefix("-") ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let tableFont = Font.regular(15.0) + let tableTextColor = theme.list.itemPrimaryTextColor + let tableLinkColor = theme.list.itemAccentColor + var tableItems: [TableComponent.Item] = [] + + if let toPeer { + tableItems.append(.init( + id: "to", + title: strings.GiftLink_To, + component: AnyComponent( + Button( + content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), + action: { + if toPeer.id != accountContext.account.peerId { + component.openPeer(toPeer) + Queue.mainQueue().after(1.0, { + component.cancel(false) + }) + } + } + ) + ) + )) + } + + tableItems.append(.init( + id: "transaction", + title: "Transaction ID", + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: transactionId, font: tableFont, textColor: tableTextColor)), truncationType: .middle) + ) + )) + + tableItems.append(.init( + id: "date", + title: strings.GiftLink_Date, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) + ) + )) + + let table = table.update( + component: TableComponent( + theme: environment.theme, + items: tableItems + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: .greatestFiniteMagnitude), + transition: .immediate + ) + + let additional = additional.update( + component: BalancedTextComponent( + text: .markdown(text: additionalText, attributes: markdownAttributes), + horizontalAlignment: .center, + maximumNumberOfLines: 0, + lineSpacing: 0.1, + highlightColor: linkColor.withAlphaComponent(0.2), + highlightAction: { attributes in + if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] { + return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL) + } else { + return nil + } + }, + tapAction: { attributes, _ in + + } + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let button = button.update( + component: SolidRoundedButtonComponent( + title: buttonText, + theme: SolidRoundedButtonComponent.Theme(theme: theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 10.0, + gloss: gloss, + iconName: nil, + animationName: nil, + iconPosition: .left, + isLoading: state.inProgress, + action: { [weak state] in + if gloss { + component.action() + if let state { + state.inProgress = true + state.updated() + } + } else { + component.cancel(true) + } + } + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), + transition: context.transition + ) + + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0 + 125.0)) + ) + + context.add(star + .position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 32.0)) + ) + + var originY: CGFloat = 0.0 + originY += star.size.height - 32.0 + + context.add(description + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) + ) + originY += description.size.height + 21.0 + + context.add(table + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) + ) + originY += table.size.height + 23.0 + + context.add(additional + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + additional.size.height / 2.0)) + ) + originY += additional.size.height + 23.0 + + let buttonFrame = CGRect(origin: CGPoint(x: sideInset, y: originY), size: button.size) + context.add(button + .position(CGPoint(x: buttonFrame.midX, y: buttonFrame.midY)) + ) + + context.add(closeButton + .position(CGPoint(x: context.availableSize.width - environment.safeInsets.left - closeButton.size.width, y: 28.0)) + ) + + let contentSize = CGSize(width: context.availableSize.width, height: buttonFrame.maxY + 5.0 + environment.safeInsets.bottom) + + return contentSize + } + } +} + +private final class StarsTransactionSheetComponent: CombinedComponent { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let subject: StarsTransactionScreen.Subject + let action: () -> Void + let openPeer: (EnginePeer) -> Void + + init( + context: AccountContext, + subject: StarsTransactionScreen.Subject, + action: @escaping () -> Void, + openPeer: @escaping (EnginePeer) -> Void + ) { + self.context = context + self.subject = subject + self.action = action + self.openPeer = openPeer + } + + static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.subject != rhs.subject { + return false + } + return true + } + + static var body: Body { + let sheet = Child(SheetComponent.self) + let animateOut = StoredActionSlot(Action.self) + + return { context in + let environment = context.environment[EnvironmentType.self] + let controller = environment.controller + + let sheet = sheet.update( + component: SheetComponent( + content: AnyComponent(StarsTransactionSheetContent( + context: context.component.context, + subject: context.component.subject, + action: context.component.action, + cancel: { animate in + if animate { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else if let controller = controller() { + controller.dismiss(animated: false, completion: nil) + } + }, + openPeer: context.component.openPeer + )), + backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), + followContentSizeChanges: true, + clipsContent: true, + animateOut: animateOut + ), + 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 { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } else { + if let controller = controller() { + 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)) + ) + + return context.availableSize + } + } +} + +public class StarsTransactionScreen: ViewControllerComponentContainer { + public enum Subject: Equatable { + case transaction(StarsContext.State.Transaction) + } + + private let context: AccountContext + public var disposed: () -> Void = {} + + private let hapticFeedback = HapticFeedback() + + public init( + context: AccountContext, + subject: StarsTransactionScreen.Subject, + forceDark: Bool = false, + action: @escaping () -> Void + ) { + self.context = context + + var openPeerImpl: ((EnginePeer) -> Void)? + super.init( + context: context, + component: StarsTransactionSheetComponent( + context: context, + subject: subject, + action: action, + openPeer: { peerId in + openPeerImpl?(peerId) + } + ), + navigationBarAppearance: .none, + statusBarStyle: .ignore, + theme: forceDark ? .dark : .default + ) + + self.navigationPresentation = .flatModal + + openPeerImpl = { [weak self] peer in + guard let self, let navigationController = self.navigationController as? NavigationController else { + return + } + let _ = (context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) + ) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) + }) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposed() + } + + public override func viewDidLoad() { + super.viewDidLoad() + + self.view.disablesInteractiveModalDismiss = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.dismissAllTooltips() + } + + public func dismissAnimated() { + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { + view.dismissAnimated() + } + } + + fileprivate func dismissAllTooltips() { + self.window?.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + }) + self.forEachController({ controller in + if let controller = controller as? UndoOverlayController { + controller.dismiss() + } + return true + }) + } +} + +private final class TableComponent: CombinedComponent { + class Item: Equatable { + public let id: AnyHashable + public let title: String + public let component: AnyComponent + + public init(id: IdType, title: String, component: AnyComponent) { + self.id = AnyHashable(id) + self.title = title + self.component = component + } + + public static func == (lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.component != rhs.component { + return false + } + return true + } + } + + private let theme: PresentationTheme + private let items: [Item] + + public init(theme: PresentationTheme, items: [Item]) { + self.theme = theme + self.items = items + } + + public static func ==(lhs: TableComponent, rhs: TableComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + + final class State: ComponentState { + var cachedBorderImage: (UIImage, PresentationTheme)? + } + + func makeState() -> State { + return State() + } + + public static var body: Body { + let leftColumnBackground = Child(Rectangle.self) + let verticalBorder = Child(Rectangle.self) + let titleChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let valueChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let borderChildren = ChildMap(environment: Empty.self, keyedBy: AnyHashable.self) + let outerBorder = Child(Image.self) + + return { context in + let verticalPadding: CGFloat = 11.0 + let horizontalPadding: CGFloat = 12.0 + let borderWidth: CGFloat = 1.0 + + let backgroundColor = context.component.theme.actionSheet.opaqueItemBackgroundColor + let borderColor = backgroundColor.mixedWith(context.component.theme.list.itemBlocksSeparatorColor, alpha: 0.6) + + var leftColumnWidth: CGFloat = 0.0 + + var updatedTitleChildren: [_UpdatedChildComponent] = [] + var updatedValueChildren: [_UpdatedChildComponent] = [] + var updatedBorderChildren: [_UpdatedChildComponent] = [] + + for item in context.component.items { + let titleChild = titleChildren[item.id].update( + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: item.title, font: Font.regular(15.0), textColor: context.component.theme.list.itemPrimaryTextColor)) + )), + availableSize: context.availableSize, + transition: context.transition + ) + updatedTitleChildren.append(titleChild) + + if titleChild.size.width > leftColumnWidth { + leftColumnWidth = titleChild.size.width + } + } + + leftColumnWidth = max(100.0, leftColumnWidth + horizontalPadding * 2.0) + let rightColumnWidth = context.availableSize.width - leftColumnWidth + + var i = 0 + var rowHeights: [Int: CGFloat] = [:] + var totalHeight: CGFloat = 0.0 + + for item in context.component.items { + let titleChild = updatedTitleChildren[i] + let valueChild = valueChildren[item.id].update( + component: item.component, + availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height), + transition: context.transition + ) + updatedValueChildren.append(valueChild) + + let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0) + rowHeights[i] = rowHeight + totalHeight += rowHeight + + if i < context.component.items.count - 1 { + let borderChild = borderChildren[item.id].update( + component: AnyComponent(Rectangle(color: borderColor)), + availableSize: CGSize(width: context.availableSize.width, height: borderWidth), + transition: context.transition + ) + updatedBorderChildren.append(borderChild) + } + + i += 1 + } + + let leftColumnBackground = leftColumnBackground.update( + component: Rectangle(color: context.component.theme.list.itemInputField.backgroundColor), + availableSize: CGSize(width: leftColumnWidth, height: totalHeight), + transition: context.transition + ) + context.add( + leftColumnBackground + .position(CGPoint(x: leftColumnWidth / 2.0, y: totalHeight / 2.0)) + ) + + let borderImage: UIImage + if let (currentImage, theme) = context.state.cachedBorderImage, theme === context.component.theme { + borderImage = currentImage + } else { + let borderRadius: CGFloat = 5.0 + borderImage = generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in + let bounds = CGRect(origin: .zero, size: size) + context.setFillColor(backgroundColor.cgColor) + context.fill(bounds) + + let path = CGPath(roundedRect: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0), cornerWidth: borderRadius, cornerHeight: borderRadius, transform: nil) + context.setBlendMode(.clear) + context.addPath(path) + context.fillPath() + + context.setBlendMode(.normal) + context.setStrokeColor(borderColor.cgColor) + context.setLineWidth(borderWidth) + context.addPath(path) + context.strokePath() + })!.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5) + context.state.cachedBorderImage = (borderImage, context.component.theme) + } + + let outerBorder = outerBorder.update( + component: Image(image: borderImage), + availableSize: CGSize(width: context.availableSize.width, height: totalHeight), + transition: context.transition + ) + context.add(outerBorder + .position(CGPoint(x: context.availableSize.width / 2.0, y: totalHeight / 2.0)) + ) + + let verticalBorder = verticalBorder.update( + component: Rectangle(color: borderColor), + availableSize: CGSize(width: borderWidth, height: totalHeight), + transition: context.transition + ) + context.add( + verticalBorder + .position(CGPoint(x: leftColumnWidth - borderWidth / 2.0, y: totalHeight / 2.0)) + ) + + i = 0 + var originY: CGFloat = 0.0 + for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) { + let rowHeight = rowHeights[i] ?? 0.0 + + let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) + let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size) + + context.add(titleChild + .position(titleFrame.center) + ) + + context.add(valueChild + .position(valueFrame.center) + ) + + if i < updatedBorderChildren.count { + let borderChild = updatedBorderChildren[i] + context.add(borderChild + .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + rowHeight - borderWidth / 2.0)) + ) + } + + originY += rowHeight + i += 1 + } + + return CGSize(width: context.availableSize.width, height: totalHeight) + } + } +} + +private final class PeerCellComponent: Component { + let context: AccountContext + let textColor: UIColor + let peer: EnginePeer? + + init(context: AccountContext, textColor: UIColor, peer: EnginePeer?) { + self.context = context + self.textColor = textColor + self.peer = peer + } + + static func ==(lhs: PeerCellComponent, rhs: PeerCellComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.textColor !== rhs.textColor { + return false + } + if lhs.peer != rhs.peer { + return false + } + return true + } + + final class View: UIView { + private let avatarNode: AvatarNode + private let text = ComponentView() + + private var component: PeerCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: PeerCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + self.avatarNode.setPeer( + context: component.context, + theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, + peer: component.peer, + synchronousLoad: true + ) + + let avatarSize = CGSize(width: 22.0, height: 22.0) + let spacing: CGFloat = 6.0 + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.peer?.compactDisplayTitle ?? "", font: Font.regular(15.0), textColor: component.textColor, paragraphAlignment: .left)) + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - avatarSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: avatarSize.width + textSize.width + spacing, height: textSize.height) + + let avatarFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - avatarSize.height) / 2.0)), size: avatarSize) + self.avatarNode.frame = avatarFrame + + if let view = self.text.view { + if view.superview == nil { + self.addSubview(view) + } + let textFrame = CGRect(origin: CGPoint(x: avatarSize.width + spacing, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + transition.setFrame(view: view, frame: textFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + + +private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { + return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setFillColor(backgroundColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setStrokeColor(foregroundColor.cgColor) + + context.move(to: CGPoint(x: 10.0, y: 10.0)) + context.addLine(to: CGPoint(x: 20.0, y: 20.0)) + context.strokePath() + + context.move(to: CGPoint(x: 20.0, y: 10.0)) + context.addLine(to: CGPoint(x: 10.0, y: 20.0)) + context.strokePath() + }) +} diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index b51cac1047..1e5c355b07 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -226,6 +226,12 @@ final class StarsTransactionsListPanelComponent: Component { case .fragment: itemTitle = "Fragment" itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor) + case .premiumBot: + itemTitle = "Premium Bot" + itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor) + case .unsupported: + itemTitle = "Unsupported" + itemLabel = NSAttributedString(string: "+ \(item.transaction.count)", font: Font.medium(fontBaseDisplaySize), textColor: environment.theme.list.itemDisclosureActions.constructive.fillColor) } itemSubtitle = stringForMediumCompactDate(timestamp: item.transaction.date, strings: environment.strings, dateTimeFormat: environment.dateTimeFormat) @@ -245,15 +251,15 @@ final class StarsTransactionsListPanelComponent: Component { AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: itemSubtitle, - font: Font.regular(floor(fontBaseDisplaySize * 14.0 / 17.0)), + font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 0, lineSpacing: 0.18 ))) - ], alignment: .left, spacing: 2.0)), - contentInsets: UIEdgeInsets(top: 11.0, left: 0.0, bottom: 11.0, right: 0.0), - leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.transaction.peer)))), + ], alignment: .left, spacing: 3.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), + leftIcon: .custom(AnyComponentWithIdentity(id: "avatar", component: AnyComponent(AvatarComponent(context: component.context, theme: environment.theme, peer: item.transaction.peer))), false), icon: nil, accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: "label", component: AnyComponent(LabelComponent(text: itemLabel))), insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 16.0))), action: { [weak self] _ in @@ -324,13 +330,14 @@ final class StarsTransactionsListPanelComponent: Component { AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( string: "abc", - font: Font.regular(floor(fontBaseDisplaySize * 13.0 / 17.0)), + font: Font.regular(floor(fontBaseDisplaySize * 15.0 / 17.0)), textColor: environment.theme.list.itemSecondaryTextColor )), maximumNumberOfLines: 0, lineSpacing: 0.18 ))) - ], alignment: .left, spacing: 2.0)), + ], alignment: .left, spacing: 3.0)), + contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 8.0, right: 0.0), leftIcon: nil, icon: nil, accessory: nil, @@ -479,6 +486,16 @@ private final class AvatarComponent: Component { self.backgroundView.isHidden = false self.iconView.isHidden = false self.avatarNode.isHidden = true + case .premiumBot: + self.backgroundView.image = gradientImage + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true + case .unsupported: + self.backgroundView.image = gradientImage + self.backgroundView.isHidden = false + self.iconView.isHidden = false + self.avatarNode.isHidden = true } self.avatarNode.frame = CGRect(origin: .zero, size: size) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 5009a08b8d..4522f76b66 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -22,15 +22,18 @@ final class StarsTransactionsScreenComponent: Component { let context: AccountContext let starsContext: StarsContext + let openTransaction: (StarsContext.State.Transaction) -> Void let buy: () -> Void init( context: AccountContext, starsContext: StarsContext, + openTransaction: @escaping (StarsContext.State.Transaction) -> Void, buy: @escaping () -> Void ) { self.context = context self.starsContext = starsContext + self.openTransaction = openTransaction self.buy = buy } @@ -243,38 +246,6 @@ final class StarsTransactionsScreenComponent: Component { } } - -// if let headerView = self.headerView.view, let navigationMetrics = self.navigationMetrics { -// var headerOffset: CGFloat = scrollBounds.minY -// -// let minY = navigationMetrics.statusBarHeight + floor((navigationMetrics.navigationHeight - navigationMetrics.statusBarHeight) / 2.0) -// -// let minOffset = headerView.center.y - minY -// -// headerOffset = min(headerOffset, minOffset) -// -// let animatedTransition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) -// let navigationBackgroundAlpha: CGFloat = abs(headerOffset - minOffset) < 4.0 ? 1.0 : 0.0 -// -// animatedTransition.setAlpha(view: self.navigationBackgroundView, alpha: navigationBackgroundAlpha) -// animatedTransition.setAlpha(layer: self.navigationSeparatorLayerContainer, alpha: navigationBackgroundAlpha) -// -// let expansionDistance: CGFloat = 32.0 -// var expansionDistanceFactor: CGFloat = abs(scrollBounds.maxY - self.scrollView.contentSize.height) / expansionDistance -// expansionDistanceFactor = max(0.0, min(1.0, expansionDistanceFactor)) -// -// transition.setAlpha(layer: self.navigationSeparatorLayer, alpha: expansionDistanceFactor) -// if let panelContainerView = self.panelContainer.view as? StarsTransactionsPanelContainerComponent.View { -// panelContainerView.updateNavigationMergeFactor(value: 1.0 - expansionDistanceFactor, transition: transition) -// } -// -// var offsetFraction: CGFloat = abs(headerOffset - minOffset) / 60.0 -// offsetFraction = min(1.0, max(0.0, offsetFraction)) -// transition.setScale(view: headerView, scale: 1.0 * offsetFraction + 0.8 * (1.0 - offsetFraction)) -// -// transition.setBounds(view: self.headerOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: headerOffset), size: self.headerOffsetContainer.bounds.size)) -// } - let _ = self.panelContainer.updateEnvironment( transition: transition, environment: { @@ -409,12 +380,12 @@ final class StarsTransactionsScreenComponent: Component { environment: {}, containerSize: CGSize(width: min(414.0, availableSize.width), height: 220.0) ) - let starFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: starSize) + let starFrame = CGRect(origin: .zero, size: starSize) if let starView = self.starView.view { if starView.superview == nil { self.insertSubview(starView, aboveSubview: self.scrollView) } - starTransition.setFrame(view: starView, frame: starFrame) + starTransition.setBounds(view: starView, bounds: starFrame) } let titleSize = self.titleView.update( @@ -543,38 +514,45 @@ final class StarsTransactionsScreenComponent: Component { ) var panelItems: [StarsTransactionsPanelContainerComponent.Item] = [] - panelItems.append(StarsTransactionsPanelContainerComponent.Item( - id: "all", - title: "All Transactions", - panel: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - items: allItems, - action: { _ in - } + if !allItems.items.isEmpty { + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "all", + title: "All Transactions", + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + items: allItems, + action: { transaction in + component.openTransaction(transaction) + } + )) )) - )) - - panelItems.append(StarsTransactionsPanelContainerComponent.Item( - id: "incoming", - title: "Incoming", - panel: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - items: incomingItems, - action: { _ in - } - )) - )) - - panelItems.append(StarsTransactionsPanelContainerComponent.Item( - id: "outgoing", - title: "Outgoing", - panel: AnyComponent(StarsTransactionsListPanelComponent( - context: component.context, - items: outgoingItems, - action: { _ in - } - )) - )) + + if !outgoingItems.items.isEmpty { + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "incoming", + title: "Incoming", + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + items: incomingItems, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + + panelItems.append(StarsTransactionsPanelContainerComponent.Item( + id: "outgoing", + title: "Outgoing", + panel: AnyComponent(StarsTransactionsListPanelComponent( + context: component.context, + items: outgoingItems, + action: { transaction in + component.openTransaction(transaction) + } + )) + )) + } + } var panelTransition = transition if balanceUpdated { @@ -655,30 +633,53 @@ final class StarsTransactionsScreenComponent: Component { public final class StarsTransactionsScreen: ViewControllerComponentContainer { private let context: AccountContext + private let starsContext: StarsContext private let options = Promise<[StarsTopUpOption]>() public init(context: AccountContext, starsContext: StarsContext, forceDark: Bool = false) { self.context = context + self.starsContext = starsContext var buyImpl: (() -> Void)? - super.init(context: context, component: StarsTransactionsScreenComponent(context: context, starsContext: starsContext, buy: { - buyImpl?() - }), navigationBarAppearance: .transparent) + var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)? + super.init(context: context, component: StarsTransactionsScreenComponent( + context: context, + starsContext: starsContext, + openTransaction: { transaction in + openTransactionImpl?(transaction) + }, + buy: { + buyImpl?() + } + ), navigationBarAppearance: .transparent) self.options.set(.single([]) |> then(context.engine.payments.starsTopUpOptions())) + openTransactionImpl = { [weak self] transaction in + guard let self else { + return + } + let controller = context.sharedContext.makeStarsTransactionScreen(context: context, transaction: transaction) + self.push(controller) + } + buyImpl = { [weak self] in guard let self else { return } let _ = (self.options.get() - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] options in + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] options in guard let self else { return } - let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil) + let controller = context.sharedContext.makeStarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: nil, requiredStars: nil, completion: { [weak self] stars in + guard let self else { + return + } + self.starsContext.add(balance: stars) + }) self.push(controller) }) } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index 6981b5290e..f09f5e029c 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -21,20 +21,26 @@ private final class SheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment let context: AccountContext + let starsContext: StarsContext let invoice: TelegramMediaInvoice let source: BotPaymentInvoiceSource let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + let dismiss: () -> Void init( context: AccountContext, + starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, - inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> + inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>, + dismiss: @escaping () -> Void ) { self.context = context + self.starsContext = starsContext self.invoice = invoice self.source = source self.inputData = inputData + self.dismiss = dismiss } static func ==(lhs: SheetContent, rhs: SheetContent) -> Bool { @@ -54,21 +60,27 @@ private final class SheetContent: CombinedComponent { private let context: AccountContext private let source: BotPaymentInvoiceSource + private let invoice: TelegramMediaInvoice - var peer: EnginePeer? - var peerDisposable: Disposable? - var balance: Int64? - var form: BotPaymentForm? + private(set) var peer: EnginePeer? + private var peerDisposable: Disposable? + private(set) var balance: Int64? + private(set) var form: BotPaymentForm? + + private var optionsDisposable: Disposable? + private(set) var options: [StarsTopUpOption] = [] var inProgress = false init( context: AccountContext, source: BotPaymentInvoiceSource, + invoice: TelegramMediaInvoice, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> ) { self.context = context self.source = source + self.invoice = invoice super.init() @@ -81,29 +93,51 @@ private final class SheetContent: CombinedComponent { self.form = inputData?.1 self.peer = inputData?.2 self.updated(transition: .immediate) + + if self.optionsDisposable != nil { + self.optionsDisposable = (context.engine.payments.starsTopUpOptions() + |> deliverOnMainQueue).start(next: { [weak self] options in + guard let self else { + return + } + self.options = options + }) + } }) } deinit { self.peerDisposable?.dispose() + self.optionsDisposable?.dispose() } - func buy(completion: @escaping () -> Void) { - guard let form else { + func buy(requestTopUp: (@escaping () -> Void) -> Void, completion: @escaping () -> Void) { + guard let form, let balance else { return } - self.inProgress = true - self.updated() - let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source) - |> deliverOnMainQueue).start(next: { _ in - completion() - }) + let action = { + self.inProgress = true + self.updated() + + let _ = (self.context.engine.payments.sendStarsPaymentForm(formId: form.id, source: self.source) + |> deliverOnMainQueue).start(next: { _ in + completion() + }) + } + + if balance < self.invoice.totalAmount { + requestTopUp({ + action() + }) + } else { + action() + } } } func makeState() -> State { - return State(context: self.context, source: self.source, inputData: self.inputData) + return State(context: self.context, source: self.source, invoice: self.invoice, inputData: self.inputData) } static var body: Body { @@ -143,11 +177,12 @@ private final class SheetContent: CombinedComponent { context: context.component.context, theme: environment.theme, peers: [peer], + photo: component.invoice.photo, isVisible: true, hasIdleAnimations: true, hasScaleAnimation: false, - color: UIColor(rgb: 0xf7ab04), - offset: 40.0 + avatarSize: 90.0, + color: UIColor(rgb: 0xf7ab04) ), availableSize: CGSize(width: min(414.0, context.availableSize.width), height: 220.0), transition: context.transition @@ -169,7 +204,7 @@ private final class SheetContent: CombinedComponent { component: Button( content: AnyComponent(Image(image: closeImage)), action: { -// component.dismiss() + component.dismiss() } ), availableSize: CGSize(width: 30.0, height: 30.0), @@ -205,7 +240,7 @@ private final class SheetContent: CombinedComponent { let amount = component.invoice.totalAmount let text = text.update( component: BalancedTextComponent( - text: .markdown(text: "Do you want to buy **\(component.invoice.title)** in **\(state.peer?.compactDisplayTitle ?? "levlam_bot")** for **\(amount) Stars**?", attributes: markdownAttributes), + text: .markdown(text: "Do you want to buy **\(component.invoice.title)** in **\(state.peer?.compactDisplayTitle ?? "")** for **\(amount) Stars**?", attributes: markdownAttributes), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -220,11 +255,11 @@ private final class SheetContent: CombinedComponent { contentSize.height += 24.0 if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== theme { - state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903))!, theme) + state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Star"), color: UIColor(rgb: 0xf09903))!, theme) } - let balanceAttributedString = parseMarkdownIntoAttributedString("Balance\n > **\(state.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString - if let range = balanceAttributedString.string.range(of: ">"), let chevronImage = state.cachedChevronImage?.0 { + let balanceAttributedString = parseMarkdownIntoAttributedString("Balance\n # **\(state.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + if let range = balanceAttributedString.string.range(of: "#"), let chevronImage = state.cachedChevronImage?.0 { balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: balanceAttributedString.string)) @@ -233,8 +268,7 @@ private final class SheetContent: CombinedComponent { component: MultilineTextComponent( text: .plain(balanceAttributedString), horizontalAlignment: .left, - maximumNumberOfLines: 0, - lineSpacing: 0.2 + maximumNumberOfLines: 0 ), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate @@ -255,10 +289,10 @@ private final class SheetContent: CombinedComponent { } let controller = environment.controller() as? StarsTransferScreen - + let accountContext = component.context + let starsContext = component.starsContext let botTitle = state.peer?.compactDisplayTitle ?? "" - let invoice = component.invoice let button = button.update( component: ButtonComponent( @@ -275,7 +309,17 @@ private final class SheetContent: CombinedComponent { isEnabled: true, displaysProgress: state.inProgress, action: { [weak state, weak controller] in - state?.buy(completion: { [weak controller] in + state?.buy(requestTopUp: { [weak controller] _ in + let purchaseController = accountContext.sharedContext.makeStarsPurchaseScreen( + context: accountContext, + starsContext: starsContext, + options: state?.options ?? [], + peerId: state?.peer?.id, + requiredStars: invoice.totalAmount, + completion: { _ in } + ) + controller?.push(purchaseController) + }, completion: { [weak controller] in let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } let resultController = UndoOverlayController( presentationData: presentationData, @@ -308,17 +352,20 @@ private final class StarsTransferSheetComponent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment private let context: AccountContext + private let starsContext: StarsContext private let invoice: TelegramMediaInvoice private let source: BotPaymentInvoiceSource private let inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> init( context: AccountContext, + starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> ) { self.context = context + self.starsContext = starsContext self.invoice = invoice self.source = source self.inputData = inputData @@ -347,12 +394,21 @@ private final class StarsTransferSheetComponent: CombinedComponent { component: SheetComponent( content: AnyComponent(SheetContent( context: context.component.context, + starsContext: context.component.starsContext, invoice: context.component.invoice, source: context.component.source, - inputData: context.component.inputData + inputData: context.component.inputData, + dismiss: { + animateOut.invoke(Action { _ in + if let controller = controller() { + controller.dismiss(completion: nil) + } + }) + } )), backgroundColor: .blur(.light), followContentSizeChanges: true, + clipsContent: true, animateOut: animateOut ), environment: { @@ -395,6 +451,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { public init( context: AccountContext, + starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError> @@ -405,6 +462,7 @@ public final class StarsTransferScreen: ViewControllerComponentContainer { context: context, component: StarsTransferSheetComponent( context: context, + starsContext: starsContext, invoice: invoice, source: source, inputData: inputData diff --git a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift index e89d933895..3b4af64ec1 100644 --- a/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift +++ b/submodules/TelegramUI/Components/TextLoadingEffect/Sources/TextLoadingEffect.swift @@ -20,8 +20,8 @@ public final class TextLoadingEffectView: UIView { private let backgroundView: UIImageView private let borderBackgroundView: UIImageView - private let duration: Double - private let gradientWidth: CGFloat + private var duration: Double + private var gradientWidth: CGFloat private var size: CGSize? @@ -112,6 +112,33 @@ public final class TextLoadingEffectView: UIView { self.borderBackgroundView.layer.add(animation, forKey: "shimmer") } + public func update(color: UIColor, rect: CGRect) { + let maskFrame = CGRect(origin: CGPoint(), size: rect.size).insetBy(dx: -4.0, dy: -4.0) + + self.gradientWidth = 260.0 + self.duration = 1.2 + + self.maskContentsView.backgroundColor = .clear + + self.backgroundView.alpha = 0.25 + self.backgroundView.tintColor = color + + self.maskContentsView.frame = maskFrame + + let rectsSet: [CGRect] = [rect] + + self.maskHighlightNode.updateRects(rectsSet) + self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize()) + + if self.size != maskFrame.size { + self.size = maskFrame.size + + self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height)) + + self.updateAnimations(size: maskFrame.size) + } + } + public func update(color: UIColor, textNode: TextNode, range: NSRange) { var rectsSet: [CGRect] = [] if let cachedLayout = textNode.cachedLayout { diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/Contents.json new file mode 100644 index 0000000000..114b9d9975 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "android.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/android.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Android.imageset/android.pdf new file mode 100644 index 0000000000000000000000000000000000000000..16f23def6eb00575f20bc9dca75ee933e94c08be GIT binary patch literal 1355 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-yDZbl(*Nfjz&qi`)+Fxc^RD zp^(i^F=~s)X6=Pjf7G4Y?3u1o^eN=h-K(Kbe}4J;^8Ne2FOP3Oe|!4;x0k4sBslwZ9xYR~_DI`r57hq?8+*ABP&-q-jQk~U>O!<59NJs01s zdn7cw%#U{_w^!oKO`g|97Dh}sr!e>Sy&ct;e7?+{H3%*v3z zoA-2!%achyIb6Ovi<{NLSe>TqvS@p~&r^eF02@99^U62nxdG^he z+hHq90+XJWhi80flf9H?5fXKewNa?KdK1^74L)X*p4t?fOY8lbz@Bly_Tqt!Cq!j7 zw0^z7x@qpuLvs@6z4$5N;xuQ9qV@&ljWg^7l#b8gGE?;nX60Nh%@Djns&37lgRV=2 zG;C+}xm+tNyKgl%O6oY>SuN}m7q1y7`U+o zrikrkVwAOxxyw~Gi@?bICLXg8C9(PVGNzBPlN7Y(Ul$yq6pkU4g@ji%D zFf%nZHdO!$LxG`z0+^+c2NyC1x)THtLKX%<|DXvOnWG6Ag0!HjGBhwpm{n4gn3j!66r2<_KF4_`{O2FPRG&DBm KQdM>JcLM-OFY2-Y literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/Contents.json new file mode 100644 index 0000000000..097594087a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "apple.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/apple.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/apple.pdf new file mode 100644 index 0000000000..c057740f51 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Apple.imageset/apple.pdf @@ -0,0 +1,159 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 9.437500 7.750366 cm +1.000000 1.000000 1.000000 scn +17.680914 12.434765 m +17.651037 15.724161 20.369793 17.304625 20.492287 17.382303 c +18.962614 19.620049 16.584450 19.924788 15.735958 19.960640 c +13.713324 20.166788 11.786292 18.768570 10.758543 18.768570 c +9.730793 18.768570 8.147343 19.930763 6.468287 19.897900 c +4.260418 19.865036 2.225832 18.616199 1.087540 16.638380 c +-1.203983 12.664813 0.501962 6.770193 2.736719 3.540550 c +3.830197 1.963074 5.129822 0.185425 6.841742 0.251152 c +8.490921 0.316881 9.112350 1.317741 11.102121 1.317741 c +13.091892 1.317741 13.653570 0.251154 15.392379 0.284018 c +17.164051 0.319870 18.287407 1.897346 19.371922 3.480797 c +20.623743 5.312222 21.140604 7.086883 21.170481 7.179500 c +21.131641 7.194438 17.716764 8.503026 17.680914 12.434765 c +f +n +Q + +endstream +endobj + +2 0 obj + 859 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 9.437500 7.998535 cm +0.000000 0.000000 0.000000 scn +0.000000 26.001465 m +21.173431 26.001465 l +21.173431 0.000002 l +0.000000 0.000002 l +0.000000 26.001465 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 232 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 19.959717 27.893250 cm +1.000000 1.000000 1.000000 scn +3.887190 1.953926 m +4.795434 3.053379 5.407901 4.583052 5.240592 6.106750 c +3.932005 6.052973 2.348554 5.234358 1.410434 4.134906 c +0.570906 3.163921 -0.167042 1.607359 0.033130 0.113537 c +1.488113 0.000007 2.978947 0.857460 3.887190 1.953926 c +h +f +n +Q + +endstream +endobj + +7 0 obj + 392 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 40.000000 40.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000001117 00000 n +0000001139 00000 n +0000001619 00000 n +0000001641 00000 n +0000001939 00000 n +0000002387 00000 n +0000002409 00000 n +0000002582 00000 n +0000002656 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2716 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/Contents.json new file mode 100644 index 0000000000..b82158fd85 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "fragment.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/fragment.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/Fragment.imageset/fragment.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e29adc31a04be7ebb42a289894d883fd24d47925 GIT binary patch literal 1283 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~g0Xjs@4666p4R8(qUFpjKlJ;S z-_`vp5#hwiBBY?Ga_#){=>94Wwt&fAbB~<+n7%yj-}i5~+i%~#{{MgRzu(U)e*Ss= z?dy-C6Em(qpJOkkFSq*mJ-K)LuKI5cOf3Fc`TL8^uiuYezx{03J~Qgo%f)GKw>&#| z47mf>3GZ8au3z`p7hUOz4_3Z*@w&fpmFiT1qSq627d9=KH0$*q*;UMI@B9*)JI&M1 z=T=y^O>)PiUAMlU*{M5W!=~K%EqW)n{=Twmf!(LGg4$Xvz0U9EuB-g9>4Vt&jUHOj zTPGY*K4bVbd+YYce?PwaQM2b)^eIar`6*G0b+rETZQ^kGJH^yy?(Eq{-^%<<*F~R} zy%UgT+qhAnb@5c+o9RnUg0{z&pM1c#$y4^##gM2Najmc~M<1`u-FBp;JUi#*zVE)7 z4~{M6TQ|eoPw$@9cO|JO+Rt9i+40e}-0!3!>ls(Cn&*3xgwi*!nl<^-;d3*R-MDs( ze8|3Nf4J_Wf{Q_XOEd_GDj&rGs-izxsDi zzpM!fhkl>5={#e9O)&Aiis|j0mn%{nxbFqBC`Bycnzi;?TnUr>8_B;66%UHOC+nJ~ z^LswO#1eYly}RW4-t{~79ODVyE^>6yqo9qA&%EbWn1tW^wN$rwUO={wVBNYoPN%l5 zvpJd>t7kklELPe0=<3APWcrI(Fz8724H|-n81Y)ESO0_sm1xFMaikf3ZSe7$`hb$;+&sXl9~s! z5tOfBf&qy|KrsbVs1Q=_0|~)%rM`D&3ec$vARh!N!0d6(F9qr~#BhHxBp1R0!wkhF zkOzz57J?iG@wjthNn%cZI;z%+qSQ1l0|j#~i1$IHf|;qQv8e)37zzvx6u>NnJh+fC z(48QF5HdFgMhTjbDbS0kLY77rXhMbt2EgEgswyc;%*;tG;sV8Y?cQ#BPd uGE+1mfubLjpI@Q?@-R4n^@B63Qh}}q7fFdlC1CFu8kiY!sj9mAy8!?}LEhT{ literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 812bf54605..55f308f31c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2915,11 +2915,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .single(nil) }) if invoice.currency == "XTR" { - let statePromise = Promise() - statePromise.set(strongSelf.context.engine.payments.peerStarsState(peerId: strongSelf.context.account.peerId)) + let starsContext = strongSelf.context.engine.payments.peerStarsContext(peerId: strongSelf.context.account.peerId) let starsInputData = combineLatest( inputData.get(), - statePromise.get() + starsContext.state ) |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in if let data, let state { @@ -2928,11 +2927,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } } - let _ = (starsInputData |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let strongSelf = self else { return } - let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, invoice: invoice, source: .message(messageId), inputData: starsInputData) + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .message(messageId), inputData: starsInputData) strongSelf.push(controller) }) } else { @@ -4684,28 +4683,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } phoneData.progress?.set(.single(true)) - let context = self.context - let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) - |> mapToSignal { peer -> Signal<(String, EnginePeer?), NoError> in - guard let peer, case let .user(user) = peer else { - return .complete() - } - var normalizedNumber = phoneData.number - if normalizedNumber.hasPrefix("0"), let accountPhone = user.phone, !accountPhone.hasPrefix("888") { - normalizedNumber = enhancePhoneNumberWithCodeFromNumber(normalizedNumber, otherPhoneNumber: accountPhone, configuration: context.currentCountriesConfiguration.with { $0 }) - } - normalizedNumber = formatPhoneNumber(context: context, number: cleanPhoneNumber(normalizedNumber)) - return self.context.engine.peers.resolvePeerByPhone(phone: normalizedNumber) - |> map { peer -> (String, EnginePeer?) in - return (normalizedNumber, peer) - } - } |> deliverOnMainQueue).start(next: { [weak self] number, peer in + let _ = (self.context.engine.peers.resolvePeerByPhone(phone: phoneData.number) + |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self else { return } phoneData.progress?.set(.single(false)) - self.openPhoneContextMenu(number: number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil) + self.openPhoneContextMenu(number: phoneData.number, peer: peer, message: phoneData.message, contentNode: phoneData.contentNode, messageNode: phoneData.messageNode, frame: phoneData.messageNode.bounds, anyRecognizer: nil, location: nil) }) }, openAgeRestrictedMessageMedia: { [weak self] message, reveal in guard let self else { diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 80d11950b0..f96d735bbf 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1723,8 +1723,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/FactCheck"), color: theme.actionSheet.primaryTextColor) }, action: { c, f in c?.dismiss(completion: { + controllerInteraction.editMessageFactCheck(messages[0].id) }) - controllerInteraction.editMessageFactCheck(messages[0].id) }))) } // if message.id.peerId.isGroupOrChannel { diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 03ea02cfba..c117929b1c 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -825,11 +825,10 @@ func openResolvedUrlImpl( return .single(nil) }) if invoice.currency == "XTR" { - let statePromise = Promise() - statePromise.set(context.engine.payments.peerStarsState(peerId: context.account.peerId)) + let starsContext = context.engine.payments.peerStarsContext(peerId: context.account.peerId) let starsInputData = combineLatest( inputData.get(), - statePromise.get() + starsContext.state ) |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in if let data, let state { @@ -838,8 +837,8 @@ func openResolvedUrlImpl( return nil } } - let _ = (starsInputData |> take(1) |> deliverOnMainQueue).start(next: { _ in - let controller = context.sharedContext.makeStarsTransferScreen(context: context, invoice: invoice, source: .slug(slug), inputData: starsInputData) + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = context.sharedContext.makeStarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: .slug(slug), inputData: starsInputData) navigationController.pushViewController(controller) }) } else { diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 9e39f17eed..8b955694ad 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -2622,12 +2622,16 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StarsTransactionsScreen(context: context, starsContext: starsContext) } - public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int32?) -> ViewController { - return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true) + public func makeStarsPurchaseScreen(context: AccountContext, starsContext: StarsContext, options: [StarsTopUpOption], peerId: EnginePeer.Id?, requiredStars: Int64?, completion: @escaping (Int64) -> Void) -> ViewController { + return StarsPurchaseScreen(context: context, starsContext: starsContext, options: options, peerId: peerId, requiredStars: requiredStars, modal: true, completion: completion) } - public func makeStarsTransferScreen(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController { - return StarsTransferScreen(context: context, invoice: invoice, source: source, inputData: inputData) + public func makeStarsTransferScreen(context: AccountContext, starsContext: StarsContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Signal<(StarsContext.State, BotPaymentForm, EnginePeer?)?, NoError>) -> ViewController { + return StarsTransferScreen(context: context, starsContext: starsContext, invoice: invoice, source: source, inputData: inputData) + } + + public func makeStarsTransactionScreen(context: AccountContext, transaction: StarsContext.State.Transaction) -> ViewController { + return StarsTransactionScreen(context: context, subject: .transaction(transaction), action: {}) } } diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 376b81e679..49c4d92664 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -865,9 +865,10 @@ public final class WebAppController: ViewController, AttachmentContainable { return .single(nil) }) if invoice.currency == "XTR" { + let starsContext = strongSelf.context.engine.payments.peerStarsContext(peerId: strongSelf.context.account.peerId) let starsInputData = combineLatest( inputData.get(), - strongSelf.context.engine.payments.peerStarsState(peerId: strongSelf.context.account.peerId) + starsContext.state ) |> map { data, state -> (StarsContext.State, BotPaymentForm, EnginePeer?)? in if let data, let state { @@ -876,8 +877,10 @@ public final class WebAppController: ViewController, AttachmentContainable { return nil } } - let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: starsInputData) - navigationController.pushViewController(controller) + let _ = (starsInputData |> filter { $0 != nil } |> take(1) |> deliverOnMainQueue).start(next: { _ in + let controller = strongSelf.context.sharedContext.makeStarsTransferScreen(context: strongSelf.context, starsContext: starsContext, invoice: invoice, source: .slug(slug), inputData: starsInputData) + navigationController.pushViewController(controller) + }) } else { let checkoutController = BotCheckoutController(context: strongSelf.context, invoice: invoice, source: .slug(slug), inputData: inputData, completed: { currencyValue, receiptMessageId in self?.sendInvoiceClosedEvent(slug: slug, result: .paid) From 41f01a5f708a5de9b11b50f9f623d8d7a39b356a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 May 2024 18:18:43 +0400 Subject: [PATCH 2/8] Various fixes --- Telegram/Telegram-iOS/en.lproj/Localizable.strings | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 8 ++------ .../Sources/Chat/ChatControllerOpenMessageFactCheck.swift | 6 +++++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 0d58780b9d..c28d37a99f 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12214,6 +12214,7 @@ Sorry for the inconvenience."; "FactCheck.Title" = "Fact Check"; "FactCheck.Placeholder" = "Add Fact Check"; +"FactCheck.Remove" = "Remove"; "Conversation.ContextMenuAddFactCheck" = "Add Fact Check"; "Conversation.ContextMenuEditFactCheck" = "Edit Fact Check"; diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 36e890b910..b47316f0f0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -209,12 +209,8 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ if let _ = invoice.extendedMedia { result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } else { - if invoice.currency == "XTR" { - result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) - } else { - skipText = true - result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) - } + skipText = true + result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default))) } needReactions = false break inner diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift index c8890dfc3f..3fefe30c17 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageFactCheck.swift @@ -20,7 +20,11 @@ extension ChatControllerImpl { guard let self else { return } - let _ = self.context.engine.messages.editMessageFactCheck(messageId: messageId, text: text, entities: entities).startStandalone() + if !currentText.isEmpty && text.isEmpty { + let _ = self.context.engine.messages.deleteMessageFactCheck(messageId: messageId).startStandalone() + } else { + let _ = self.context.engine.messages.editMessageFactCheck(messageId: messageId, text: text, entities: entities).startStandalone() + } }) self.present(controller, in: .window(.root)) } From 7c7456de4b0ef3ee08a85bb5f5866ad38abedacf Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 21 May 2024 18:32:36 +0400 Subject: [PATCH 3/8] Fix build --- .../TelegramEngine/Payments/Stars.swift | 2 -- .../Stars/StarLarge.imageset/Contents.json | 2 +- ...ancestar_48.pdf => balancestar_48 (2).pdf} | Bin 14735 -> 14697 bytes 3 files changed, 1 insertion(+), 3 deletions(-) rename submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/{balancestar_48.pdf => balancestar_48 (2).pdf} (93%) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift index 7098955b27..710a63d477 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/Stars.swift @@ -234,8 +234,6 @@ private extension StarsContext.State.Transaction { return nil } parsedPeer = .peer(EnginePeer(peer)) - case .starsTransactionPeerPremiumBot, .starsTransactionPeerUnsupported: - return nil } self.init(id: id, count: stars, date: date, peer: parsedPeer) } diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json index 0b71e559ab..ac82493b96 100644 --- a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "balancestar_48.pdf", + "filename" : "balancestar_48 (2).pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/balancestar_48.pdf b/submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/balancestar_48 (2).pdf similarity index 93% rename from submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/balancestar_48.pdf rename to submodules/TelegramUI/Images.xcassets/Premium/Stars/StarLarge.imageset/balancestar_48 (2).pdf index f55d110a3f9da4a6006b4d64cb1d02271266c410..323a72ad266b04a1f1d299d555acd011ece78ace 100644 GIT binary patch delta 990 zcmV<410np6bLn!h-!gx@ZYD(#h4XoeiIHe`RdroDg}%2wYaCI#Crms1&ws_lJ}C#Cu(*s^WR3{1CcT=y)9135kC zidg%q$O_JsiYRWGZivd!p;y9<&`<&u{X|rAY;GBZo{Uo_ZQYK^%*>R`)D@AI#6u}! zkaG2~rxgPSgSrfQ^`Wv>Hm|GPVxAgf?lno*5o>j24Z42{G{dd=UeFGdm5WAYZ1F1c z2D6bVT?{Pj73EAMVP@$Ls)3PY4soIB4)LcT*y$!HqK2+ov;~&vr7mpejI}x@=a|#< zG_=5&qB(>S2CqzSG2MyusTWzgK4IjwPE?f)X{lf6b4Q$}8EW}g=U^q8^TM8IbUTQ@k^ZI* z=E;I&FW_QviPY>ec*;lLU@&Ub1dm{WSisPJ_UkpAu`t(Lzs{V=DuWVSlzv%LnYO)^ z!Tg=&1e<$yMleZY_uP$*GJ*b690pL%rXo;=&C-80x*Qc^LY>DqCm0Mz+{}wAfjGf_ z?Imm*F{!Q>f$yEN#Yo6S!zC2Y8@R%QfDky&fkMj_$(_avj0Q2b^`J%Vc#43xuL8th zdT%7AI5s%5hBk5nJ1{mNFfcGMFd%LU zFfcGMFflVZGC3eHFfcGMAZ`jUFfcGMF*GqSHXtxCFfcG6ZVE6kFfcGNG%+|blQTI~ M2sASaB_%~qMqr`d3IG5A delta 1003 zcmVwiBnu;`{@g𝔞`TcQl zw|lS>0ZJ5%5>d6&{HtQ=^5)^+Kd%oz{qWuX{mzHm=QkTCD{ZnmVLDrM%%jCu5+q}ABza7CuA ziel0b80psRIgWp*PchlWD51lHgsKcfS=(hccVTQ&sx@;8@t%rPk}klUZj$VaU!yNA z%+yzqeYI($z9T0M+nO%BvKCcaYo|`itS`ZaEn{b3x?JP<&VoFUQ+u8fV;vpYgfpch znj5AIqOw)$k#Hjnlt7btB4%-HZW*NRtW!E;T~SdEGo^nreMO`t`A~`&q#QG>Zj*(B zL0ty3dQ%xAi`OyTqVF25uhA)28Rw|Z3Un1{hFkMBpdBb@4hEIB#OI`Lup8-8&A`GM zF`bPh%nV&YH!zayAr3U%A$}JG+ua03%%)=w#sW+9G7oHJk1;AIarADw8(LsZQ5?bu zi&wTc*lvGE`t*w~WK50#40$a0QfZOinIFtbKL>}}WDfc-R6Hgq81sZh)k#-_FEDO_ z4HfoWo7dU8dxw#GpQtKX(ojFp=ZT2A8ESboIoK4`{Y2f4sdX| zWGZ%9Jnf@zFc>v@f=94GEMRCq>-A{PTG;EgUT1&yL zt0I^rxohvnMVUbVE)D}IXVDQT%jW0`9gYeyp{nDH2nNFuH~XSXAR^eGYY5jyPP(f@ z;P)z7VkG5Y;3+iE8@R%QfDky&fkLM%lG}|J7!6`9>p_cJ`4j;!9}^INsk^DdN?Qxa z2Wo#hAZ!k9fhHFXMUcpM!Fd{&_UwXiDBSGgys;y{^pCF z`||GBj~^fYd8+@-F7M>D>pFEr$zh##UA&%lecCk`-dPpFPP_hU>{?H|u2V;p9M);q z#p`L;r(J{LomCO+wCgXn>&si&f5&g%>hTTNFS5V+iy!_2?q`f*lRq{d1354@vtTxI z0y{J`ATTg6Ffbr)3NSD*FfcJQIW{*SFfcGMFd%LUFfcGMFflYSG&CSEFfcGMAZ`jU ZFfcGMF*GtVFp~{AR0udR3MC~)PexNd=8gaW From 464eb8625f3d38d82661524ed91ec7e40fa7cf22 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 22 May 2024 14:04:37 +0400 Subject: [PATCH 4/8] Hashtag search improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 9 + .../Sources/ChatController.swift | 9 + .../DrawingUI/Sources/DrawingUtils.swift | 56 -- .../Sources/HashtagSearchController.swift | 166 ++---- .../Sources/HashtagSearchControllerNode.swift | 294 ++++++---- .../HashtagSearchGlobalChatContents.swift | 217 ++++++++ .../HashtagSearchNavigationContentNode.swift | 149 +++++ .../Sources/HashtagSearchRecentListNode.swift | 510 ++++++++++++++++++ .../Sources/HashtagSearchRecentQueries.swift | 68 +++ .../Sources/MediaDustNode.swift | 20 +- .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 5 + .../BUILD | 2 + ...ChatInlineSearchResultsListComponent.swift | 122 ++++- .../ChatMessageAnimatedStickerItemNode.swift | 1 + .../Sources/ChatMessageBubbleItemNode.swift | 25 +- .../ChatMessageInteractiveMediaNode.swift | 123 +++-- .../ChatMessageMediaBubbleContentNode.swift | 37 +- .../Sources/ChatMessageStickerItemNode.swift | 1 + .../ChatMessageTextBubbleContentNode.swift | 8 +- .../Chat/ChatMessageThreadInfoNode/BUILD | 1 + .../Sources/ChatMessageThreadInfoNode.swift | 126 +++-- .../Sources/FactCheckAlertController.swift | 2 +- .../Sources/ChatControllerInteraction.swift | 4 +- .../Sources/ChatEntityKeyboardInputNode.swift | 2 + .../Drawing/DrawingStickerEntity.swift | 62 ++- .../Sources/MediaEditorComposerEntity.swift | 49 +- .../Sources/PeerInfoChatListPaneNode.swift | 2 +- .../PeerInfoScreen/Sources/PeerInfoData.swift | 9 +- .../Sources/PeerInfoScreen.swift | 19 +- ...aticBusinessMessageSetupChatContents.swift | 12 + .../Sources/BusinessLinkChatContents.swift | 8 + .../ClearRecent.imageset/Contents.json | 12 + .../ClearRecent.imageset/clearrecents_30.pdf | 62 +++ .../Chat/Hashtag/Contents.json | 9 + .../EmptyHashtag.imageset/Contents.json | 12 + .../EmptyHashtag.imageset/tagempty_80.pdf | Bin 0 -> 1377 bytes .../RecentHashtag.imageset/Contents.json | 12 + .../RecentHashtag.imageset/tagsearch_30.pdf | Bin 0 -> 1317 bytes .../Chat/ChatControllerLoadDisplayNode.swift | 43 +- .../ChatControllerNavigateToMessage.swift | 7 +- ...UpdateChatPresentationInterfaceState.swift | 2 + .../ChatBusinessLinkTitlePanelNode.swift | 2 + .../TelegramUI/Sources/ChatController.swift | 31 +- .../Sources/ChatControllerNode.swift | 104 ++-- .../Sources/ChatInterfaceInputContexts.swift | 4 + .../ChatInterfaceStateContextMenus.swift | 2 + .../ChatInterfaceStateInputPanels.swift | 9 +- .../ChatInterfaceStateNavigationButtons.swift | 4 + .../ChatInterfaceTitlePanelNodes.swift | 2 + .../ChatRestrictedInputPanelNode.swift | 2 + .../Sources/ChatTagSearchInputPanelNode.swift | 63 ++- .../Sources/ChatTextInputPanelNode.swift | 16 + .../Sources/PostboxKeys.swift | 2 + 53 files changed, 2049 insertions(+), 469 deletions(-) create mode 100644 submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift create mode 100644 submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift create mode 100644 submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift create mode 100644 submodules/HashtagSearchUI/Sources/HashtagSearchRecentQueries.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/clearrecents_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Hashtag/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Hashtag/EmptyHashtag.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Hashtag/EmptyHashtag.imageset/tagempty_80.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Hashtag/RecentHashtag.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Hashtag/RecentHashtag.imageset/tagsearch_30.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index c28d37a99f..d8dd72eb9c 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12194,6 +12194,13 @@ Sorry for the inconvenience."; "HashtagSearch.ThisChat" = "This Chat"; "HashtagSearch.MyMessages" = "My Messages"; "HashtagSearch.PublicPosts" = "Public Posts"; +"HashtagSearch.SearchPlaceholder" = "Hashtag search"; + +"HashtagSearch.NoRecentQueries" = "Enter a hashtag to find messages\ncontaining it."; +"HashtagSearch.ClearRecent" = "Clear History"; + +"HashtagSearch.NoResults" = "No Results"; +"HashtagSearch.NoResultsQueryDescription" = "There were no results for %@.\nTry another hashtag."; "Chat.Context.Phone.AddToContacts" = "Add to Contacts"; "Chat.Context.Phone.CreateNewContact" = "Create New Contact"; @@ -12233,3 +12240,5 @@ Sorry for the inconvenience."; "Stars.Purchase.ShowMore" = "Show More Options"; "Stars.Purchase.Info" = "By proceeding and purchasing Stars, you agree with [Terms and Conditions]()."; "Stars.Purchase.Terms_URL" = "https://telegram.org/tos"; + +"Settings.Stars" = "Your Stars"; diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index ae42cad445..bf4e60edfc 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -986,7 +986,10 @@ public protocol ChatController: ViewController { var visibleContextController: ViewController? { get } + var searching: ValuePromise { get } + var alwaysShowSearchResultsAsList: Bool { get set } + var includeSavedPeersInSearchResults: Bool { get set } func updatePresentationMode(_ mode: ChatControllerPresentationMode) func beginMessageSearch(_ query: String) @@ -1102,6 +1105,7 @@ public enum ChatQuickReplyShortcutType { public enum ChatCustomContentsKind: Equatable { case quickReplyMessageInput(shortcut: String, shortcutType: ChatQuickReplyShortcutType) case businessLinkSetup(link: TelegramBusinessChatLinks.Link) + case hashTagSearch } public protocol ChatCustomContentsProtocol: AnyObject { @@ -1115,6 +1119,11 @@ public protocol ChatCustomContentsProtocol: AnyObject { func quickReplyUpdateShortcut(value: String) func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) + + func loadMore() + + func hashtagSearchUpdate(query: String) + var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void { get set } } public enum ChatHistoryListDisplayHeaders { diff --git a/submodules/DrawingUI/Sources/DrawingUtils.swift b/submodules/DrawingUI/Sources/DrawingUtils.swift index 1050beaafb..43f6047068 100644 --- a/submodules/DrawingUI/Sources/DrawingUtils.swift +++ b/submodules/DrawingUI/Sources/DrawingUtils.swift @@ -544,59 +544,3 @@ extension CATransform3D { return (t, r, s) } } - -public extension UIImage { - class func animatedImageFromData(data: Data) -> DrawingAnimatedImage? { - guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { - return nil - } - - let count = CGImageSourceGetCount(source) - var images = [UIImage]() - var duration = 0.0 - - for i in 0.. Double { - var delay = 0.0 - - let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) - let gifPropertiesPointer = UnsafeMutablePointer.allocate(capacity: 0) - if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDictionary).toOpaque(), gifPropertiesPointer) == false { - return delay - } - - let gifProperties:CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self) - - var delayObject: AnyObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyGIFUnclampedDelayTime).toOpaque()), to: AnyObject.self) - if delayObject.doubleValue == 0 { - delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyGIFDelayTime).toOpaque()), to: AnyObject.self) - } - - delay = delayObject as? Double ?? 0 - - return delay - } -} - -public final class DrawingAnimatedImage { - public let images: [UIImage] - public let duration: Double - - init(images: [UIImage], duration: Double) { - self.images = images - self.duration = duration - } -} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 5340d936df..e0c832a762 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -49,91 +49,64 @@ public final class HashtagSearchController: TelegramBaseController { self.title = query self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) - let location: SearchMessagesLocation = .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil) - let search = context.engine.messages.searchMessages(location: location, query: query, state: nil) - let foundMessages: Signal<[ChatListSearchEntry], NoError> = combineLatest(search, self.context.sharedContext.presentationData) - |> map { result, presentationData in - let result = result.0 - let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap { EnginePeerReadCounters(state: $0, isMuted: false) }, nil, chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false, nil, false) }) - } - let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { - }, peerSelected: { _, _, _, _ in - }, disabledPeerSelected: { _, _, _ in - }, togglePeerSelected: { _, _ in - }, togglePeersSelection: { _, _ in - }, additionalCategorySelected: { _ in - }, messageSelected: { [weak self] peer, _, message, _ in - if let strongSelf = self { - strongSelf.openMessageFromSearchDisposable.set((strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer) |> deliverOnMainQueue).start(next: { actualPeer in - if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: message.id.peerId == actualPeer.id ? .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) : nil, keepStack: .always)) - } - })) - strongSelf.controllerNode.listNode.clearHighlightAnimated(true) - } - }, groupSelected: { _ in - }, addContact: {_ in - }, setPeerIdWithRevealedOptions: { _, _ in - }, setItemPinned: { _, _ in - }, setPeerMuted: { _, _ in - }, setPeerThreadMuted: { _, _, _ in - }, deletePeer: { _, _ in - }, deletePeerThread: { _, _ in - }, setPeerThreadStopped: { _, _, _ in - }, setPeerThreadPinned: { _, _, _ in - }, setPeerThreadHidden: { _, _, _ in - }, updatePeerGrouping: { _, _ in - }, togglePeerMarkedUnread: { _, _ in - }, toggleArchivedFolderHiddenByDefault: { - }, toggleThreadsSelection: { _, _ in - }, hidePsa: { _ in - }, activateChatPreview: { _, _, _, gesture, _ in - gesture?.cancel() - }, present: { _ in - }, openForumThread: { _, _ in - }, openStorageManagement: { - }, openPasswordSetup: { - }, openPremiumIntro: { - }, openPremiumGift: { _ in - }, openPremiumManagement: { - }, openActiveSessions: { - }, openBirthdaySetup: { - }, performActiveSessionAction: { _, _ in - }, openChatFolderUpdates: { - }, hideChatFolderUpdates: { - }, openStories: { _, _ in - }, dismissNotice: { _ in - }, editPeer: { _ in - }) +// let location: SearchMessagesLocation = .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil) +// let search = context.engine.messages.searchMessages(location: location, query: query, state: nil) +// let foundMessages: Signal<[ChatListSearchEntry], NoError> = combineLatest(search, self.context.sharedContext.presentationData) +// |> map { result, presentationData in +// let result = result.0 +// let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) +// return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap { EnginePeerReadCounters(state: $0, isMuted: false) }, nil, chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false, nil, false) }) +// } - let previousSearchItems = Atomic<[ChatListSearchEntry]?>(value: nil) - self.transitionDisposable = (foundMessages - |> deliverOnMainQueue).start(next: { [weak self] entries in - if let strongSelf = self { - let previousEntries = previousSearchItems.swap(entries) +// let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { +// }, peerSelected: { _, _, _, _ in +// }, disabledPeerSelected: { _, _, _ in +// }, togglePeerSelected: { _, _ in +// }, togglePeersSelection: { _, _ in +// }, additionalCategorySelected: { _ in +// }, messageSelected: { [weak self] peer, _, message, _ in +// if let strongSelf = self { +// strongSelf.openMessageFromSearchDisposable.set((strongSelf.context.engine.peers.ensurePeerIsLocallyAvailable(peer: peer) |> deliverOnMainQueue).start(next: { actualPeer in +// if let strongSelf = self, let navigationController = strongSelf.navigationController as? NavigationController { +// strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(actualPeer), subject: message.id.peerId == actualPeer.id ? .message(id: .id(message.id), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil) : nil, keepStack: .always)) +// } +// })) +// } +// }, groupSelected: { _ in +// }, addContact: {_ in +// }, setPeerIdWithRevealedOptions: { _, _ in +// }, setItemPinned: { _, _ in +// }, setPeerMuted: { _, _ in +// }, setPeerThreadMuted: { _, _, _ in +// }, deletePeer: { _, _ in +// }, deletePeerThread: { _, _ in +// }, setPeerThreadStopped: { _, _, _ in +// }, setPeerThreadPinned: { _, _, _ in +// }, setPeerThreadHidden: { _, _, _ in +// }, updatePeerGrouping: { _, _ in +// }, togglePeerMarkedUnread: { _, _ in +// }, toggleArchivedFolderHiddenByDefault: { +// }, toggleThreadsSelection: { _, _ in +// }, hidePsa: { _ in +// }, activateChatPreview: { _, _, _, gesture, _ in +// gesture?.cancel() +// }, present: { _ in +// }, openForumThread: { _, _ in +// }, openStorageManagement: { +// }, openPasswordSetup: { +// }, openPremiumIntro: { +// }, openPremiumGift: { _ in +// }, openPremiumManagement: { +// }, openActiveSessions: { +// }, openBirthdaySetup: { +// }, performActiveSessionAction: { _, _ in +// }, openChatFolderUpdates: { +// }, hideChatFolderUpdates: { +// }, openStories: { _, _ in +// }, dismissNotice: { _ in +// }, editPeer: { _ in +// }) - let listInteraction = ListMessageItemInteraction(openMessage: { message, mode -> Bool in - return true - }, openMessageContextMenu: { message, bool, node, rect, gesture in - }, toggleMessagesSelection: { messageId, selected in - }, openUrl: { url, _, _, message in - }, openInstantPage: { message, data in - }, longTap: { action, message in - }, getHiddenMedia: { - return [:] - }) - - let firstTime = previousEntries == nil - let transition = chatListSearchContainerPreparedTransition(from: previousEntries ?? [], to: entries, displayingResults: true, isEmpty: entries.isEmpty, isLoading: false, animated: false, context: strongSelf.context, presentationData: strongSelf.presentationData, enableHeaders: false, filter: [], requestPeerType: nil, location: .chatList(groupId: .root), key: .chats, tagMask: nil, interaction: interaction, listInteraction: listInteraction, peerContextAction: nil, toggleExpandLocalResults: { - }, toggleExpandGlobalResults: { - }, searchPeer: { _ in - }, searchQuery: "", searchOptions: nil, messageContextAction: nil, openClearRecentlyDownloaded: {}, toggleAllPaused: {}, openStories: { _, _ in - }) - strongSelf.controllerNode.enqueueTransition(transition, firstTime: firstTime) - } - }) - self.presentationDataDisposable = (self.context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { @@ -158,23 +131,18 @@ public final class HashtagSearchController: TelegramBaseController { } deinit { - self.transitionDisposable?.dispose() self.presentationDataDisposable?.dispose() self.openMessageFromSearchDisposable.dispose() } override public func loadDisplayNode() { self.displayNode = HashtagSearchControllerNode(context: self.context, controller: self, peer: self.peer, query: self.query, navigationBar: self.navigationBar, navigationController: self.navigationController as? NavigationController) - if let chatController = self.controllerNode.chatController { + if let chatController = self.controllerNode.currentController { chatController.parentController = self } self.displayNodeDidLoad() } - - private var suspendNavigationBarLayout: Bool = false - private var suspendedNavigationBarLayout: ContainerViewLayout? - private var additionalNavigationBarBackgroundHeight: CGFloat = 0.0 private func updateThemeAndStrings() { self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style @@ -183,26 +151,10 @@ public final class HashtagSearchController: TelegramBaseController { self.controllerNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) } - - override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - if self.suspendNavigationBarLayout { - self.suspendedNavigationBarLayout = layout - return - } - self.applyNavigationBarLayout(layout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) - } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.suspendNavigationBarLayout = true - + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) - self.additionalNavigationBarBackgroundHeight = self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) - - self.suspendNavigationBarLayout = false - if let suspendedNavigationBarLayout = self.suspendedNavigationBarLayout { - self.suspendedNavigationBarLayout = suspendedNavigationBarLayout - self.applyNavigationBarLayout(suspendedNavigationBarLayout, navigationLayout: self.navigationLayout(layout: layout), additionalBackgroundHeight: self.additionalNavigationBarBackgroundHeight, transition: transition) - } + let _ = self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.cleanNavigationHeight, transition: transition) } } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index 7bf1f75596..e1fc79ae27 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -1,6 +1,7 @@ import Display import UIKit import AsyncDisplayKit +import SwiftSignalKit import TelegramCore import TelegramPresentationData import AccountContext @@ -11,18 +12,28 @@ import ChatListSearchItemHeader final class HashtagSearchControllerNode: ASDisplayNode { private let context: AccountContext private weak var controller: HashtagSearchController? - private let query: String + private var query: String + + private let searchQueryPromise = ValuePromise() + private var searchQueryDisposable: Disposable? private let navigationBar: NavigationBar? - private let segmentedControlNode: SegmentedControlNode - let listNode: ListView - let shimmerNode: ChatListSearchShimmerNode + private let searchContentNode: HashtagSearchNavigationContentNode + private let shimmerNode: ChatListSearchShimmerNode + private let recentListNode: HashtagSearchRecentListNode - let chatController: ChatController? + private let isSearching = Promise() + private var isSearchingDisposable: Disposable? + + let currentController: ChatController? + let myController: ChatController? + let myChatContents: HashtagSearchGlobalChatContents? + + let globalController: ChatController? + let globalChatContents: HashtagSearchGlobalChatContents? private var containerLayout: (ContainerViewLayout, CGFloat)? - private var enqueuedTransitions: [(ChatListSearchContainerTransition, Bool)] = [] private var hasValidLayout = false init(context: AccountContext, controller: HashtagSearchController, peer: EnginePeer?, query: String, navigationBar: NavigationBar?, navigationController: NavigationController?) { @@ -33,32 +44,40 @@ final class HashtagSearchControllerNode: ASDisplayNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let cleanHashtag = query.replacingOccurrences(of: "#", with: "") + self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: cleanHashtag, cancel: { [weak controller] in + controller?.dismiss() + }) + self.shimmerNode = ChatListSearchShimmerNode(key: .chats) self.shimmerNode.isUserInteractionEnabled = false self.shimmerNode.allowsGroupOpacity = true - self.listNode = ListView() - self.listNode.accessibilityPageScrolledString = { row, count in - return presentationData.strings.VoiceOver_ScrollStatus(row, count).string - } - - var items: [String] = [] - if peer?.id == context.account.peerId { - items.append(presentationData.strings.Conversation_SavedMessages) - } else if let id = peer?.id, id.isReplies { - items.append(presentationData.strings.DialogList_Replies) + self.recentListNode = HashtagSearchRecentListNode(context: context) + + let navigationController = controller.navigationController as? NavigationController + if let peer { + self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController)) + self.currentController?.alwaysShowSearchResultsAsList = true + self.currentController?.customNavigationController = navigationController } else { - items.append(peer?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? "") + self.currentController = nil } - items.append(presentationData.strings.HashtagSearch_AllChats) - self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: presentationData.theme), items: items.map { SegmentedControlItem(title: $0) }, selectedIndex: controller.all ? 1 : 0) - if let peer = peer { - self.chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController)) - } else { - self.chatController = nil - } - + self.isSearching.set(self.currentController?.searching.get() ?? .single(false)) + + let myChatContents = HashtagSearchGlobalChatContents(context: context, kind: .hashTagSearch, query: cleanHashtag, onlyMy: true) + self.myChatContents = myChatContents + self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default)) + self.myController?.alwaysShowSearchResultsAsList = true + self.myController?.customNavigationController = navigationController + + let globalChatContents = HashtagSearchGlobalChatContents(context: context, kind: .hashTagSearch, query: cleanHashtag, onlyMy: false) + self.globalChatContents = globalChatContents + self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default)) + self.globalController?.alwaysShowSearchResultsAsList = true + self.globalController?.customNavigationController = navigationController + super.init() self.setViewBlock({ @@ -66,135 +85,206 @@ final class HashtagSearchControllerNode: ASDisplayNode { }) self.backgroundColor = presentationData.theme.chatList.backgroundColor - - self.addSubnode(self.listNode) -// self.addSubnode(self.shimmerNode) - + if controller.all { - self.chatController?.displayNode.isHidden = true - self.listNode.isHidden = false + self.currentController?.displayNode.isHidden = true } else { - self.chatController?.displayNode.isHidden = false - self.listNode.isHidden = true + self.currentController?.displayNode.isHidden = false + self.myController?.displayNode.isHidden = true + self.globalController?.displayNode.isHidden = true } - self.segmentedControlNode.selectedIndexChanged = { [weak self] index in - if let strongSelf = self { - if index == 0 { - strongSelf.chatController?.displayNode.isHidden = false - strongSelf.listNode.isHidden = true - } else { - strongSelf.chatController?.displayNode.isHidden = true - strongSelf.listNode.isHidden = false - } + self.searchContentNode.indexUpdated = { [weak self] index in + guard let self else { + return + } + self.searchContentNode.selectedIndex = index + if index == 0 { + self.currentController?.displayNode.isHidden = false + self.myController?.displayNode.isHidden = true + self.globalController?.displayNode.isHidden = true + self.isSearching.set(self.currentController?.searching.get() ?? .single(false)) + } else if index == 1 { + self.currentController?.displayNode.isHidden = true + self.myController?.displayNode.isHidden = false + self.globalController?.displayNode.isHidden = true + self.isSearching.set(self.myChatContents?.searching ?? .single(false)) + } else if index == 2 { + self.currentController?.displayNode.isHidden = true + self.myController?.displayNode.isHidden = true + self.globalController?.displayNode.isHidden = false + self.isSearching.set(self.globalChatContents?.searching ?? .single(false)) } } + + self.recentListNode.setSearchQuery = { [weak self] query in + guard let self else { + return + } + self.searchContentNode.query = query + self.updateSearchQuery(query) + } - self.chatController?.isSelectingMessagesUpdated = { [weak self] isSelecting in + self.currentController?.isSelectingMessagesUpdated = { [weak self] isSelecting in if let strongSelf = self { let button: UIBarButtonItem? = isSelecting ? UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .done, target: self, action: #selector(strongSelf.cancelPressed)) : nil strongSelf.controller?.navigationItem.setRightBarButton(button, animated: true) } } - } - - @objc private func cancelPressed() { - self.chatController?.cancelSelectingMessages() - } - - func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { - self.backgroundColor = theme.chatList.backgroundColor - self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: theme)) + navigationBar?.setContentNode(self.searchContentNode, animated: false) - self.listNode.forEachItemHeaderNode({ itemHeaderNode in - if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode { - itemHeaderNode.updateTheme(theme: theme) + self.addSubnode(self.shimmerNode) + + self.searchContentNode.setQueryUpdated { [weak self] query in + self?.searchQueryPromise.set(query) + } + + let _ = addRecentHashtagSearchQuery(engine: context.engine, string: query).startStandalone() + self.searchContentNode.onReturn = { query in + let _ = addRecentHashtagSearchQuery(engine: context.engine, string: query).startStandalone() + } + + let throttledSearchQuery = self.searchQueryPromise.get() + |> mapToSignal { query -> Signal in + if !query.isEmpty { + return (.complete() |> delay(1.0, queue: Queue.mainQueue())) + |> then(.single(query)) + } else { + return .single(query) + } + } + + self.searchQueryDisposable = (throttledSearchQuery + |> deliverOnMainQueue).start(next: { [weak self] query in + if let self { + self.updateSearchQuery(query) + } + }) + + self.isSearchingDisposable = (self.isSearching.get() + |> deliverOnMainQueue).start(next: { [weak self] isSearching in + if let self { + self.searchContentNode.isSearching = isSearching + let transition: ContainedViewLayoutTransition = isSearching ? .immediate : .animated(duration: 0.2, curve: .easeInOut) + transition.updateAlpha(node: self.shimmerNode, alpha: isSearching ? 1.0 : 0.0) } }) } - func enqueueTransition(_ transition: ChatListSearchContainerTransition, firstTime: Bool) { - self.enqueuedTransitions.append((transition, firstTime)) + deinit { + self.searchQueryDisposable?.dispose() + self.isSearchingDisposable?.dispose() + } + + func updateSearchQuery(_ query: String) { + self.query = query - if self.hasValidLayout { - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } + var cleanQuery = query + if cleanQuery.hasPrefix("#") { + cleanQuery.removeFirst() + } + if !cleanQuery.isEmpty { + self.currentController?.beginMessageSearch("#" + cleanQuery) + + self.myChatContents?.hashtagSearchUpdate(query: cleanQuery) + self.myController?.beginMessageSearch("#" + cleanQuery) + + self.globalChatContents?.hashtagSearchUpdate(query: cleanQuery) + self.globalController?.beginMessageSearch("#" + cleanQuery) + } + + if let (layout, navigationHeight) = self.containerLayout { + let _ = self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate) } } - private func dequeueTransition() { - if let (transition, _) = self.enqueuedTransitions.first { - self.enqueuedTransitions.remove(at: 0) - - let options = ListViewDeleteAndInsertOptions() - self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) - } + @objc private func cancelPressed() { + self.currentController?.cancelSelectingMessages() + } + + func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.backgroundColor = theme.chatList.backgroundColor + self.searchContentNode.updateTheme(theme) } func scrollToTop() { - if self.segmentedControlNode.selectedIndex == 0 { - self.chatController?.scrollToTop?() + if self.searchContentNode.selectedIndex == 0 { + self.currentController?.scrollToTop?() + } else if self.searchContentNode.selectedIndex == 2 { + self.globalController?.scrollToTop?() } else { - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.myController?.scrollToTop?() } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat { + let isFirstTime = self.containerLayout == nil self.containerLayout = (layout, navigationBarHeight) - - if self.chatController != nil && self.segmentedControlNode.supernode == nil { - self.navigationBar?.additionalContentNode.addSubnode(self.segmentedControlNode) - } - + var insets = layout.insets(options: [.input]) insets.top += navigationBarHeight let toolbarHeight: CGFloat = 40.0 - let panelY: CGFloat = insets.top - UIScreenPixel - 4.0 - - let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: layout.size.width - 14.0 * 2.0), transition: transition) - transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: panelY + 2.0 + floor((toolbarHeight - controlSize.height) / 2.0)), size: controlSize)) - - if let chatController = self.chatController { - insets.top += toolbarHeight - 4.0 - let chatSize = CGSize(width: layout.size.width, height: layout.size.height) - transition.updateFrame(node: chatController.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: chatSize)) - chatController.containerLayoutUpdated(ContainerViewLayout(size: chatSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate) - if chatController.displayNode.supernode == nil { - chatController.viewWillAppear(false) - self.insertSubnode(chatController.displayNode, at: 0) - chatController.viewDidAppear(false) + insets.top += toolbarHeight - 4.0 + if let controller = self.currentController { + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 79.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + + if controller.displayNode.supernode == nil { + controller.viewWillAppear(false) + self.insertSubnode(controller.displayNode, at: 0) + controller.viewDidAppear(false) - chatController.beginMessageSearch(self.query) + controller.beginMessageSearch(self.query) } } - self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + if let controller = self.myController { + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 89.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + + if controller.displayNode.supernode == nil { + controller.viewWillAppear(false) + self.insertSubnode(controller.displayNode, at: 0) + controller.viewDidAppear(false) + + controller.beginMessageSearch(self.query) + } + } + + if let controller = self.globalController { + transition.updateFrame(node: controller.displayNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)) + controller.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 89.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + + if controller.displayNode.supernode == nil { + controller.viewWillAppear(false) + self.insertSubnode(controller.displayNode, at: 0) + controller.viewDidAppear(false) + + controller.beginMessageSearch(self.query) + } + } let overflowInset: CGFloat = 0.0 let topInset = navigationBarHeight self.shimmerNode.frame = CGRect(origin: CGPoint(x: overflowInset, y: topInset), size: CGSize(width: layout.size.width - overflowInset * 2.0, height: layout.size.height)) self.shimmerNode.update(context: self.context, size: CGSize(width: layout.size.width - overflowInset * 2.0, height: layout.size.height), presentationData: self.context.sharedContext.currentPresentationData.with { $0 }, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, key: .chats, hasSelection: false, transition: transition) - insets.top += 4.0 + if isFirstTime { + self.insertSubnode(self.recentListNode, aboveSubnode: self.shimmerNode) + } - let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) - let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve) - - self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.recentListNode.frame = CGRect(origin: .zero, size: layout.size) + self.recentListNode.updateLayout(layout: ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: insets.top - 35.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right), safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition) + self.recentListNode.isHidden = !self.query.isEmpty if !self.hasValidLayout { self.hasValidLayout = true - while !self.enqueuedTransitions.isEmpty { - self.dequeueTransition() - } } - if self.chatController != nil { + if self.currentController != nil { return toolbarHeight } else { return 0.0 diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift new file mode 100644 index 0000000000..f7acade603 --- /dev/null +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchGlobalChatContents.swift @@ -0,0 +1,217 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Postbox +import TelegramCore +import AccountContext + +final class HashtagSearchGlobalChatContents: ChatCustomContentsProtocol { + private final class Impl { + let queue: Queue + let context: AccountContext + + fileprivate var query: String { + didSet { + if self.query != oldValue { + self.updateHistoryViewRequest(reload: true) + } + } + } + private let onlyMy: Bool + private var currentSearchState: SearchMessagesState? + + private(set) var mergedHistoryView: MessageHistoryView? + private var sourceHistoryView: MessageHistoryView? + + private var historyViewDisposable: Disposable? + let historyViewStream = ValuePipe<(MessageHistoryView, ViewUpdateType)>() + private var nextUpdateIsHoleFill: Bool = false + + var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void = { _ in } + + let isSearchingPromise = ValuePromise(true) + + init(queue: Queue, context: AccountContext, query: String, onlyMy: Bool) { + self.queue = queue + self.context = context + self.query = query + self.onlyMy = onlyMy + + self.updateHistoryViewRequest(reload: false) + } + + deinit { + self.historyViewDisposable?.dispose() + } + + private func updateHistoryViewRequest(reload: Bool) { + guard self.historyViewDisposable == nil || reload else { + return + } + self.historyViewDisposable?.dispose() + + let search: Signal<(SearchMessagesResult, SearchMessagesState), NoError> + if self.onlyMy { + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: "#\(self.query)", state: nil) + } else { + search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: nil) + } + + self.isSearchingPromise.set(true) + self.historyViewDisposable = (search + |> deliverOn(self.queue)).start(next: { [weak self] result in + guard let self else { + return + } + + let updateType: ViewUpdateType = .Initial + + let historyView = MessageHistoryView(tag: nil, namespaces: .just(Set([Namespaces.Message.Cloud])), entries: result.0.messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: false, holeLater: false, isLoading: false) + self.sourceHistoryView = historyView + self.updateHistoryView(updateType: updateType) + + Queue.mainQueue().async { + self.currentSearchState = result.1 + + self.hashtagSearchResultsUpdate(result) + } + + self.historyViewDisposable?.dispose() + self.historyViewDisposable = nil + + self.isSearchingPromise.set(false) + }) + } + + private func updateHistoryView(updateType: ViewUpdateType) { + var entries = self.sourceHistoryView?.entries ?? [] + entries.sort(by: { $0.message.index < $1.message.index }) + + let mergedHistoryView = MessageHistoryView(tag: nil, namespaces: .just(Set([Namespaces.Message.Cloud])), entries: entries, holeEarlier: false, holeLater: false, isLoading: false) + self.mergedHistoryView = mergedHistoryView + + self.historyViewStream.putNext((mergedHistoryView, updateType)) + } + + func loadMore() { + guard self.historyViewDisposable == nil, let currentSearchState = self.currentSearchState else { + return + } + + let search: Signal<(SearchMessagesResult, SearchMessagesState), NoError> + if self.onlyMy { + search = self.context.engine.messages.searchMessages(location: .general(scope: .everywhere, tags: nil, minDate: nil, maxDate: nil), query: "#\(self.query)", state: currentSearchState) + } else { + search = self.context.engine.messages.searchHashtagPosts(hashtag: self.query, state: self.currentSearchState) + } + + self.historyViewDisposable?.dispose() + self.historyViewDisposable = (search + |> deliverOn(self.queue)).startStrict(next: { [weak self] result in + guard let self else { + return + } + + let updateType: ViewUpdateType = .FillHole + + let historyView = MessageHistoryView(tag: nil, namespaces: .just(Set([Namespaces.Message.Cloud])), entries: result.0.messages.reversed().map { MessageHistoryEntry(message: $0, isRead: false, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) }, holeEarlier: false, holeLater: false, isLoading: false) + self.sourceHistoryView = historyView + + self.updateHistoryView(updateType: updateType) + + Queue.mainQueue().async { + self.currentSearchState = result.1 + + self.hashtagSearchResultsUpdate(result) + } + + self.historyViewDisposable?.dispose() + self.historyViewDisposable = nil + }) + } + + func enqueueMessages(messages: [EnqueueMessage]) { + } + + func deleteMessages(ids: [EngineMessage.Id]) { + + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + } + } + + var kind: ChatCustomContentsKind + + var historyView: Signal<(MessageHistoryView, ViewUpdateType), NoError> { + return self.impl.signalWith({ impl, subscriber in + if let mergedHistoryView = impl.mergedHistoryView { + subscriber.putNext((mergedHistoryView, .Initial)) + } + return impl.historyViewStream.signal().start(next: subscriber.putNext) + }) + } + + var searching: Signal { + return self.impl.signalWith({ impl, subscriber in + return impl.isSearchingPromise.get().start(next: subscriber.putNext) + }) + } + + var messageLimit: Int? { + return nil + } + + private let queue: Queue + private let impl: QueueLocalObject + + init(context: AccountContext, kind: ChatCustomContentsKind, query: String, onlyMy: Bool) { + self.kind = kind + + let queue = Queue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue, context: context, query: query, onlyMy: onlyMy) + }) + } + + func enqueueMessages(messages: [EnqueueMessage]) { + + } + + func deleteMessages(ids: [EngineMessage.Id]) { + + } + + func editMessage(id: EngineMessage.Id, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) { + + } + + func quickReplyUpdateShortcut(value: String) { + + } + + func businessLinkUpdate(message: String, entities: [TelegramCore.MessageTextEntity], title: String?) { + + } + + func loadMore() { + self.impl.with { impl in + impl.loadMore() + } + } + + var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void = { _ in } { + didSet { + self.impl.with { impl in + impl.hashtagSearchResultsUpdate = self.hashtagSearchResultsUpdate + } + } + } + + func hashtagSearchUpdate(query: String) { + self.impl.with { impl in + impl.query = query + } + } +} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift new file mode 100644 index 0000000000..151e77629d --- /dev/null +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -0,0 +1,149 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore +import TelegramPresentationData +import SearchBarNode +import ComponentFlow +import ComponentDisplayAdapters +import TabSelectorComponent + +private let searchBarFont = Font.regular(17.0) + +final class HashtagSearchNavigationContentNode: NavigationBarContentNode { + private var theme: PresentationTheme + private let strings: PresentationStrings + + private let cancel: () -> Void + + var onReturn: (String) -> Void = { _ in } + + private let searchBar: SearchBarNode + private let tabSelector = ComponentView() + + private var queryUpdated: ((String) -> Void)? + var indexUpdated: ((Int) -> Void)? + + var selectedIndex: Int = 0 { + didSet { + if let (size, leftInset, rightInset) = self.validLayout { + self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset, transition: .animated(duration: 0.35, curve: .spring)) + } + } + } + + var isSearching: Bool = false { + didSet { + self.searchBar.activity = self.isSearching + } + } + + var query: String { + get { + return self.searchBar.text + } + set { + self.searchBar.text = newValue + } + } + + init(theme: PresentationTheme, strings: PresentationStrings, initialQuery: String, cancel: @escaping () -> Void) { + self.theme = theme + self.strings = strings + + self.cancel = cancel + + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, icon: .hashtag, displayBackground: false) + self.searchBar.text = initialQuery + self.searchBar.placeholderString = NSAttributedString(string: strings.HashtagSearch_SearchPlaceholder, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + + super.init() + + self.addSubnode(self.searchBar) + + self.searchBar.cancel = { [weak self] in + self?.searchBar.deactivate(clear: false) + self?.cancel() + } + + self.searchBar.textUpdated = { [weak self] query, _ in + self?.queryUpdated?(query) + } + + self.searchBar.textReturned = { [weak self] query in + self?.onReturn(query) + } + } + + func updateTheme(_ theme: PresentationTheme) { + self.theme = theme + self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: self.strings) + } + + func setQueryUpdated(_ f: @escaping (String) -> Void) { + self.queryUpdated = f + } + + override var nominalHeight: CGFloat { + return 54.0 + 44.0 + } + + private var validLayout: (CGSize, CGFloat, CGFloat)? + + override func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, leftInset, rightInset) + + let sideInset: CGFloat = 6.0 + + let searchBarFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - self.nominalHeight + 5.0), size: CGSize(width: size.width, height: 54.0)) + self.searchBar.frame = searchBarFrame + self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset + sideInset, rightInset: rightInset + sideInset, transition: transition) + + let tabSelectorSize = self.tabSelector.update( + transition: Transition(transition), + component: AnyComponent(TabSelectorComponent( + colors: TabSelectorComponent.Colors( + foreground: self.theme.list.itemSecondaryTextColor, + selection: self.theme.list.itemAccentColor + ), + customLayout: TabSelectorComponent.CustomLayout( + font: Font.medium(14.0), + spacing: 24.0, + lineSelection: true + ), + items: [ + TabSelectorComponent.Item(id: AnyHashable(0), title: self.strings.HashtagSearch_ThisChat), + TabSelectorComponent.Item(id: AnyHashable(1), title: self.strings.HashtagSearch_MyMessages), + TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts) + ], + selectedId: AnyHashable(self.selectedIndex), + setSelectedId: { [weak self] id in + guard let self, let index = id.base as? Int else { + return + } + self.indexUpdated?(index) + }, + transitionFraction: 0.0 + )), + environment: {}, + containerSize: CGSize(width: size.width, height: 44.0) + ) + let tabSelectorFrame = CGRect(origin: CGPoint(x: floor((size.width - tabSelectorSize.width) / 2.0), y: size.height - tabSelectorSize.height - 9.0), size: tabSelectorSize) + if let tabSelectorView = self.tabSelector.view { + if tabSelectorView.superview == nil { + self.view.addSubview(tabSelectorView) + } + transition.updateFrame(view: tabSelectorView, frame: tabSelectorFrame) + } + } + + func activate() { + self.searchBar.activate() + } + + func deactivate() { + self.searchBar.deactivate(clear: false) + } +} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift new file mode 100644 index 0000000000..4ce0ebb0cd --- /dev/null +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift @@ -0,0 +1,510 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import SwiftSignalKit +import Display +import TelegramCore +import TelegramPresentationData +import ItemListUI +import MergeLists +import AccountContext + +final class HashtagSearchInteraction { + let setSearchQuery: (String) -> Void + let deleteRecentQuery: (String) -> Void + let clearRecentQueries: () -> Void + + init(setSearchQuery: @escaping (String) -> Void, deleteRecentQuery: @escaping (String) -> Void, clearRecentQueries: @escaping () -> Void) { + self.setSearchQuery = setSearchQuery + self.deleteRecentQuery = deleteRecentQuery + self.clearRecentQueries = clearRecentQueries + } +} + +private enum HashtagSearchRecentQueryStableId: Hashable { + case query(String) + case clear +} + +private enum HashtagSearchRecentQueryEntry: Comparable, Identifiable { + case query(index: Int, text: String) + case clear + + var stableId: HashtagSearchRecentQueryStableId { + switch self { + case let .query(_, text): + return .query(text) + case .clear: + return .clear + } + } + + static func ==(lhs: HashtagSearchRecentQueryEntry, rhs: HashtagSearchRecentQueryEntry) -> Bool { + switch lhs { + case let .query(lhsIndex, lhsText): + if case let .query(rhsIndex, rhsText) = rhs { + return lhsIndex == rhsIndex && lhsText == rhsText + } else { + return false + } + case .clear: + if case .clear = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: HashtagSearchRecentQueryEntry, rhs: HashtagSearchRecentQueryEntry) -> Bool { + switch lhs { + case let .query(lhsIndex, _): + switch rhs { + case let .query(rhsIndex, _): + return lhsIndex < rhsIndex + case .clear: + return true + } + case .clear: + switch rhs { + case .query: + return false + case .clear: + return true + } + } + } + + func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: HashtagSearchInteraction) -> ListViewItem { + var isClear = false + let text: String + switch self { + case let .query(_, value): + text = value + case .clear: + isClear = true + text = strings.HashtagSearch_ClearRecent + } + return HashtagSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: text, clear: isClear, tapped: { query in + if isClear { + interaction.clearRecentQueries() + } else { + interaction.setSearchQuery(text) + } + }, deleted: { query in + interaction.deleteRecentQuery(query) + }) + } +} + +private struct HashtagSearchRecentTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isEmpty: Bool +} + +private func preparedHashtagSearchRecentTransition(from fromEntries: [HashtagSearchRecentQueryEntry], to toEntries: [HashtagSearchRecentQueryEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: HashtagSearchInteraction) -> HashtagSearchRecentTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction), directionHint: nil) } + + return HashtagSearchRecentTransition(deletions: deletions, insertions: insertions, updates: updates, isEmpty: toEntries.isEmpty) +} + +private enum RevealOptionKey: Int32 { + case delete +} + +public class HashtagSearchRecentQueryItem: ListViewItem { + let theme: PresentationTheme + let strings: PresentationStrings + let account: Account + let query: String + let clear: Bool + let tapped: (String) -> Void + let deleted: (String) -> Void + + let header: ListViewItemHeader? = nil + + public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, query: String, clear: Bool, tapped: @escaping (String) -> Void, deleted: @escaping (String) -> Void) { + self.theme = theme + self.strings = strings + self.account = account + self.query = query + self.clear = clear + self.tapped = tapped + self.deleted = deleted + } + + public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { + async { + let node = HashtagSearchRecentQueryItemNode() + let makeLayout = node.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(self, params, nextItem == nil, !(previousItem is HashtagSearchRecentQueryItem)) + node.contentSize = nodeLayout.contentSize + node.insets = nodeLayout.insets + + completion(node, nodeApply) + } + } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { + Queue.mainQueue().async { + if let nodeValue = node() as? HashtagSearchRecentQueryItemNode { + let layout = nodeValue.asyncLayout() + async { + let (nodeLayout, apply) = layout(self, params, nextItem == nil, !(previousItem is HashtagSearchRecentQueryItem)) + Queue.mainQueue().async { + completion(nodeLayout, { info in + apply().1(info) + }) + } + } + } + } + } + + public var selectable: Bool { + return true + } + + public func selected(listView: ListView) { + listView.clearHighlightAnimated(true) + self.tapped(self.query) + } +} + +final class HashtagSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { + private let backgroundNode: ASDisplayNode + private let separatorNode: ASDisplayNode + private let highlightedBackgroundNode: ASDisplayNode + private var textNode: TextNode? + private let iconNode: ASImageNode + + private var item: HashtagSearchRecentQueryItem? + private var layoutParams: ListViewItemLayoutParams? + + required init() { + self.backgroundNode = ASDisplayNode() + self.backgroundNode.isLayerBacked = true + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + + self.highlightedBackgroundNode = ASDisplayNode() + self.highlightedBackgroundNode.isLayerBacked = true + + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + + super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false) + + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.iconNode) + } + + override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { + if let item = self.item { + let makeLayout = self.asyncLayout() + let (nodeLayout, nodeApply) = makeLayout(item, params, nextItem == nil, previousItem == nil) + self.contentSize = nodeLayout.contentSize + self.insets = nodeLayout.insets + let _ = nodeApply() + } + } + + override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) { + super.setHighlighted(highlighted, at: point, animated: animated) + + if highlighted { + self.highlightedBackgroundNode.alpha = 1.0 + if self.highlightedBackgroundNode.supernode == nil { + self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode) + } + } else { + if self.highlightedBackgroundNode.supernode != nil { + if animated { + self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in + if let strongSelf = self { + if completed { + strongSelf.highlightedBackgroundNode.removeFromSupernode() + } + } + }) + self.highlightedBackgroundNode.alpha = 0.0 + } else { + self.highlightedBackgroundNode.removeFromSupernode() + } + } + } + } + + func asyncLayout() -> (_ item: HashtagSearchRecentQueryItem, _ params: ListViewItemLayoutParams, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, (ListViewItemApply) -> Void)) { + let currentItem = self.item + + let textLayout = TextNode.asyncLayout(self.textNode) + + return { [weak self] item, params, last, firstWithHeader in + + let leftInset: CGFloat = 62.0 + params.leftInset + let rightInset: CGFloat = params.rightInset + + let attributedString = NSAttributedString(string: item.query, font: Font.regular(17.0), textColor: item.clear ? item.theme.list.itemAccentColor : item.theme.list.itemPrimaryTextColor) + let textApply = textLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + + let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + + return (nodeLayout, { [weak self] in + var updatedTheme: PresentationTheme? + if currentItem?.theme !== item.theme { + updatedTheme = item.theme + } + + return (nil, { _ in + if let strongSelf = self { + strongSelf.item = item + strongSelf.layoutParams = params + + if let _ = updatedTheme { + strongSelf.separatorNode.backgroundColor = item.theme.list.itemPlainSeparatorColor + strongSelf.backgroundNode.backgroundColor = item.theme.list.plainBackgroundColor + strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor + if item.clear { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Hashtag/ClearRecent"), color: item.theme.list.itemAccentColor) + } else { + strongSelf.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Hashtag/RecentHashtag"), color: item.theme.list.itemSecondaryTextColor) + } + } + + let (textLayout, textApply) = textApply + let textNode = textApply() + if strongSelf.textNode == nil { + strongSelf.textNode = textNode + strongSelf.addSubnode(textNode) + } + + let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + textNode.frame = textFrame + + if let icon = strongSelf.iconNode.image { + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - icon.size.width - 16.0, y: floorToScreenPixels((nodeLayout.contentSize.height - icon.size.height) / 2.0)), size: icon.size) + } + + let separatorHeight = UIScreenPixel + let topHighlightInset: CGFloat = separatorHeight + + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width, height: separatorHeight)) + strongSelf.separatorNode.isHidden = last + + strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) + + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + } + }) + }) + } + } + + override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false) + } + + override public func headers() -> [ListViewItemHeader]? { + if let item = self.item { + return item.header.flatMap { [$0] } + } else { + return nil + } + } + + override func updateRevealOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { + super.updateRevealOffset(offset: offset, transition: transition) + + if let params = self.layoutParams, let textNode = self.textNode { + let leftInset: CGFloat = 15.0 + params.leftInset + + var textFrame = textNode.frame + textFrame.origin.x = leftInset + offset + transition.updateFrame(node: textNode, frame: textFrame) + } + } + + override func revealOptionSelected(_ option: ItemListRevealOption, animated: Bool) { + if let item = self.item { + switch option.key { + case RevealOptionKey.delete.rawValue: + item.deleted(item.query) + default: + break + } + } + self.setRevealOptionsOpened(false, animated: true) + self.revealOptionsInteractivelyClosed() + } +} + +final class HashtagSearchRecentListNode: ASDisplayNode { + private let context: AccountContext + private var presentationData: PresentationData + + private let listNode: ListView + + private let emptyIconNode: ASImageNode + private let emptyTextNode: ImmediateTextNode + + private var enqueuedRecentTransitions: [(HashtagSearchRecentTransition, Bool)] = [] + private var recentDisposable: Disposable? + + private var validLayout: ContainerViewLayout? + + private var interaction: HashtagSearchInteraction? + + var setSearchQuery: (String) -> Void = { _ in } + + init(context: AccountContext) { + self.context = context + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.presentationData = presentationData + + self.listNode = ListView() + self.listNode.accessibilityPageScrolledString = { row, count in + return presentationData.strings.VoiceOver_ScrollStatus(row, count).string + } + + self.emptyIconNode = ASImageNode() + self.emptyIconNode.displaysAsynchronously = false + + self.emptyTextNode = ImmediateTextNode() + self.emptyTextNode.displaysAsynchronously = false + self.emptyTextNode.maximumNumberOfLines = 0 + self.emptyTextNode.textAlignment = .center + + super.init() + + self.addSubnode(self.listNode) + self.addSubnode(self.emptyIconNode) + self.addSubnode(self.emptyTextNode) + + self.interaction = HashtagSearchInteraction( + setSearchQuery: { [weak self] query in + self?.setSearchQuery(query) + }, + deleteRecentQuery: { query in + let _ = removeRecentHashtagSearchQuery(engine: context.engine, string: query).startStandalone() + }, + clearRecentQueries: { + let _ = clearRecentHashtagSearchQueries(engine: context.engine).startStandalone() + } + ) + + self.listNode.beganInteractiveDragging = { [weak self] _ in + self?.view.window?.endEditing(true) + } + + let previousRecentItems = Atomic<[HashtagSearchRecentQueryEntry]?>(value: nil) + self.recentDisposable = (hashtagSearchRecentQueries(engine: self.context.engine) + |> deliverOnMainQueue).start(next: { [weak self] queries in + guard let self else { + return + } + var entries: [HashtagSearchRecentQueryEntry] = [] + for i in 0 ..< queries.count { + entries.append(.query(index: i, text: queries[i])) + } + + if !entries.isEmpty { + entries.append(.clear) + } + + let previousEntries = previousRecentItems.swap(entries) + + let transition = preparedHashtagSearchRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: self.presentationData.theme, strings: self.presentationData.strings, interaction: self.interaction!) + self.enqueueRecentTransition(transition, firstTime: previousEntries == nil) + }) + + self.updatePresentationData(self.presentationData) + } + + deinit { + self.recentDisposable?.dispose() + } + + private func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + + self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + self.listNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor + + self.emptyIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Hashtag/EmptyHashtag"), color: self.presentationData.theme.list.freeMonoIconColor) + self.emptyTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.HashtagSearch_NoRecentQueries, font: Font.regular(15.0), textColor: self.presentationData.theme.list.freeTextColor) + } + + private func enqueueRecentTransition(_ transition: HashtagSearchRecentTransition, firstTime: Bool) { + enqueuedRecentTransitions.append((transition, firstTime)) + + if let _ = self.validLayout { + while !self.enqueuedRecentTransitions.isEmpty { + self.dequeueRecentTransition() + } + } + } + + private func dequeueRecentTransition() { + if let (transition, firstTime) = self.enqueuedRecentTransitions.first { + self.enqueuedRecentTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + if firstTime { + options.insert(.PreferSynchronousDrawing) + } else { + options.insert(.AnimateInsertion) + } + + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + guard let self else { + return + } + + self.emptyIconNode.isHidden = !transition.isEmpty + self.emptyTextNode.isHidden = !transition.isEmpty + }) + } + } + + func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + self.validLayout = layout + + let insets: UIEdgeInsets = layout.insets(options: [.input]) + + let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if let emptyIconSize = self.emptyIconNode.image?.size { + let topInset: CGFloat = insets.top + let bottomInset: CGFloat = insets.bottom + + let sideInset: CGFloat = 0.0 + let padding: CGFloat = 16.0 + let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) + + let emptyTextSpacing: CGFloat = 6.0 + let emptyTotalHeight = emptyIconSize.height + emptyTextSpacing + emptyTextSize.height + let emptyOriginY = topInset + floorToScreenPixels((layout.size.height - topInset - bottomInset - emptyTotalHeight) / 2.0) + + transition.updateFrame(node: self.emptyIconNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (layout.size.width - sideInset * 2.0 - padding * 2.0 - emptyIconSize.width) / 2.0, y: emptyOriginY), size: emptyIconSize)) + transition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: sideInset + padding + (layout.size.width - sideInset * 2.0 - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyOriginY + emptyIconSize.height + emptyTextSpacing), size: emptyTextSize)) + } + } +} diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchRecentQueries.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentQueries.swift new file mode 100644 index 0000000000..bca1b83497 --- /dev/null +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentQueries.swift @@ -0,0 +1,68 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import TelegramUIPreferences + +private struct HashtagSearchRecentQueryItemId { + public let rawValue: MemoryBuffer + + var value: String { + return String(data: self.rawValue.makeData(), encoding: .utf8) ?? "" + } + + init(_ rawValue: MemoryBuffer) { + self.rawValue = rawValue + } + + init?(_ value: String) { + if let data = value.data(using: .utf8) { + self.rawValue = MemoryBuffer(data: data) + } else { + return nil + } + } +} + +public final class RecentHashtagSearchQueryItem: Codable { + public init() { + } + + public init(from decoder: Decoder) throws { + } + + public func encode(to encoder: Encoder) throws { + } +} + +func addRecentHashtagSearchQuery(engine: TelegramEngine, string: String) -> Signal { + if let itemId = HashtagSearchRecentQueryItemId(string) { + return engine.orderedLists.addOrMoveToFirstPosition(collectionId: ApplicationSpecificOrderedItemListCollectionId.hashtagSearchRecentQueries, id: itemId.rawValue, item: RecentHashtagSearchQueryItem(), removeTailIfCountExceeds: 100) + } else { + return .complete() + } +} + +func removeRecentHashtagSearchQuery(engine: TelegramEngine, string: String) -> Signal { + if let itemId = HashtagSearchRecentQueryItemId(string) { + return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.hashtagSearchRecentQueries, id: itemId.rawValue) + } else { + return .complete() + } +} + +func clearRecentHashtagSearchQueries(engine: TelegramEngine) -> Signal { + return engine.orderedLists.clear(collectionId: ApplicationSpecificOrderedItemListCollectionId.hashtagSearchRecentQueries) +} + +func hashtagSearchRecentQueries(engine: TelegramEngine) -> Signal<[String], NoError> { + return engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.hashtagSearchRecentQueries)) + |> map { items -> [String] in + var result: [String] = [] + for item in items { + let value = HashtagSearchRecentQueryItemId(item.id).value + result.append(value) + } + return result + } +} diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index 31ad26614b..d6af2f9e1a 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -211,7 +211,7 @@ public class MediaDustNode: ASDisplayNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))) } - @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { + public func tap(at location: CGPoint) { guard !self.isRevealed else { return } @@ -223,13 +223,12 @@ public class MediaDustNode: ASDisplayNode { if self.enableAnimations { self.isExploding = true - let position = gestureRecognizer.location(in: self.view) self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") - self.emitterLayer?.setValue(position, forKeyPath: "emitterBehaviors.fingerAttractor.position") + self.emitterLayer?.setValue(location, forKeyPath: "emitterBehaviors.fingerAttractor.position") let maskSize = self.emitterNode.frame.size Queue.concurrentDefaultQueue().async { - let emitterMaskImage = generateMaskImage(size: maskSize, position: position, inverse: true) + let emitterMaskImage = generateMaskImage(size: maskSize, position: location, inverse: true) Queue.mainQueue().async { self.emitterSpotNode.image = emitterMaskImage @@ -237,8 +236,8 @@ public class MediaDustNode: ASDisplayNode { } Queue.mainQueue().after(0.1 * UIView.animationDurationFactor()) { - let xFactor = (position.x / self.emitterNode.frame.width - 0.5) * 2.0 - let yFactor = (position.y / self.emitterNode.frame.height - 0.5) * 2.0 + let xFactor = (location.x / self.emitterNode.frame.width - 0.5) * 2.0 + let yFactor = (location.y / self.emitterNode.frame.height - 0.5) * 2.0 let maxFactor = max(abs(xFactor), abs(yFactor)) let scaleAddition = maxFactor * 4.0 @@ -247,8 +246,8 @@ public class MediaDustNode: ASDisplayNode { self.supernode?.view.mask = self.emitterMaskNode.view self.emitterSpotNode.frame = CGRect(x: 0.0, y: 0.0, width: self.emitterMaskNode.frame.width * 3.0, height: self.emitterMaskNode.frame.height * 3.0) - self.emitterSpotNode.layer.anchorPoint = CGPoint(x: position.x / self.emitterMaskNode.frame.width, y: position.y / self.emitterMaskNode.frame.height) - self.emitterSpotNode.position = position + self.emitterSpotNode.layer.anchorPoint = CGPoint(x: location.x / self.emitterMaskNode.frame.width, y: location.y / self.emitterMaskNode.frame.height) + self.emitterSpotNode.position = location self.emitterSpotNode.layer.animateScale(from: 0.3333, to: 10.5 + scaleAddition, duration: 0.45 + durationAddition, removeOnCompletion: false, completion: { [weak self] _ in self?.revealed() self?.alpha = 0.0 @@ -272,6 +271,11 @@ public class MediaDustNode: ASDisplayNode { }) } } + + @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { + let location = gestureRecognizer.location(in: self.view) + self.tap(at: location) + } private var didSetupAnimations = false private func setupRandomAnimations() { diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index 218ca14ac6..ac62e628a6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -779,6 +779,8 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC insets.top = -9.0 imageSpacing = 4.0 titleSpacing = 5.0 + case .hashTagSearch: + break } } @@ -838,6 +840,9 @@ private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeC } self.businessLink = link + case .hashTagSearch: + titleString = "" + strings = [] } } else { titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD index f82054dbad..c9dbe087ce 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/BUILD @@ -27,6 +27,8 @@ swift_library( "//submodules/ContactsPeerItem", "//submodules/ItemListUI", "//submodules/ChatListSearchItemHeader", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/Components/MultilineTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift index dff94d1759..d8a51a421e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInlineSearchResultsListComponent/Sources/ChatInlineSearchResultsListComponent.swift @@ -16,6 +16,8 @@ import ChatPresentationInterfaceState import ContactsPeerItem import ItemListUI import ChatListSearchItemHeader +import LottieComponent +import MultilineTextComponent public final class ChatInlineSearchResultsListComponent: Component { public struct Presentation: Equatable { @@ -73,9 +75,11 @@ public final class ChatInlineSearchResultsListComponent: Component { public let context: AccountContext public let presentation: Presentation - public let peerId: EnginePeer.Id + public let peerId: EnginePeer.Id? public let contents: Contents public let insets: UIEdgeInsets + public let inputHeight: CGFloat + public let showEmptyResults: Bool public let messageSelected: (EngineMessage) -> Void public let peerSelected: (EnginePeer) -> Void public let loadTagMessages: (MemoryBuffer, MessageIndex?) -> Signal? @@ -86,9 +90,11 @@ public final class ChatInlineSearchResultsListComponent: Component { public init( context: AccountContext, presentation: Presentation, - peerId: EnginePeer.Id, + peerId: EnginePeer.Id?, contents: Contents, insets: UIEdgeInsets, + inputHeight: CGFloat, + showEmptyResults: Bool, messageSelected: @escaping (EngineMessage) -> Void, peerSelected: @escaping (EnginePeer) -> Void, loadTagMessages: @escaping (MemoryBuffer, MessageIndex?) -> Signal?, @@ -101,6 +107,8 @@ public final class ChatInlineSearchResultsListComponent: Component { self.peerId = peerId self.contents = contents self.insets = insets + self.inputHeight = inputHeight + self.showEmptyResults = showEmptyResults self.messageSelected = messageSelected self.peerSelected = peerSelected self.loadTagMessages = loadTagMessages @@ -125,6 +133,12 @@ public final class ChatInlineSearchResultsListComponent: Component { if lhs.insets != rhs.insets { return false } + if lhs.inputHeight != rhs.inputHeight { + return false + } + if lhs.showEmptyResults != rhs.showEmptyResults { + return false + } return true } @@ -216,6 +230,9 @@ public final class ChatInlineSearchResultsListComponent: Component { private var isUpdating: Bool = false private let listNode: ListView + private let emptyResultsTitle = ComponentView() + private let emptyResultsText = ComponentView() + private let emptyResultsAnimation = ComponentView() private var tagContents: (index: MessageIndex?, disposable: Disposable?)? private var searchContents: (index: MessageIndex?, disposable: Disposable?)? @@ -257,6 +274,10 @@ public final class ChatInlineSearchResultsListComponent: Component { self.searchContents?.disposable?.dispose() } + public func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } + public func animateIn() { self.listNode.layer.animateSublayerScale(from: 0.95, to: 1.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) @@ -846,6 +867,103 @@ public final class ChatInlineSearchResultsListComponent: Component { } } + let fadeTransition = Transition.easeInOut(duration: 0.25) + if component.showEmptyResults, let appliedContentsState = self.appliedContentsState, appliedContentsState.entries.isEmpty, case let .search(query, _) = component.contents, !query.isEmpty { + let sideInset: CGFloat = 44.0 + let emptyAnimationHeight = 148.0 + let topInset: CGFloat = component.insets.top + let bottomInset: CGFloat = max(component.insets.bottom, component.inputHeight) + let visibleHeight = availableSize.height + let emptyAnimationSpacing: CGFloat = 8.0 + let emptyTextSpacing: CGFloat = 8.0 + + let emptyResultsTitleSize = self.emptyResultsTitle.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.presentation.strings.HashtagSearch_NoResults, font: Font.semibold(17.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center + ) + ), + environment: {}, + containerSize: availableSize + ) + let emptyResultsTextSize = self.emptyResultsText.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.presentation.strings.HashtagSearch_NoResultsQueryDescription(query).string, font: Font.regular(15.0), textColor: component.presentation.theme.list.itemSecondaryTextColor)), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height) + ) + let emptyResultsAnimationSize = self.emptyResultsAnimation.update( + transition: .immediate, + component: AnyComponent(LottieComponent( + content: LottieComponent.AppBundleContent(name: "ChatListNoResults") + )), + environment: {}, + containerSize: CGSize(width: emptyAnimationHeight, height: emptyAnimationHeight) + ) + + let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyResultsTitleSize.height + emptyResultsTextSize.height + emptyTextSpacing + let emptyAnimationY = topInset + floorToScreenPixels((visibleHeight - topInset - bottomInset - emptyTotalHeight) / 2.0) + + let emptyResultsAnimationFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsAnimationSize.width) / 2.0), y: emptyAnimationY), size: emptyResultsAnimationSize) + + let emptyResultsTitleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTitleSize.width) / 2.0), y: emptyResultsAnimationFrame.maxY + emptyAnimationSpacing), size: emptyResultsTitleSize) + + let emptyResultsTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - emptyResultsTextSize.width) / 2.0), y: emptyResultsTitleFrame.maxY + emptyTextSpacing), size: emptyResultsTextSize) + + if let view = self.emptyResultsAnimation.view as? LottieComponent.View { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.addSubview(view) + view.playOnce() + } + view.bounds = CGRect(origin: .zero, size: emptyResultsAnimationFrame.size) + transition.setPosition(view: view, position: emptyResultsAnimationFrame.center) + } + if let view = self.emptyResultsTitle.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsTitleFrame.size) + transition.setPosition(view: view, position: emptyResultsTitleFrame.center) + } + if let view = self.emptyResultsText.view { + if view.superview == nil { + view.alpha = 0.0 + fadeTransition.setAlpha(view: view, alpha: 1.0) + self.addSubview(view) + } + view.bounds = CGRect(origin: .zero, size: emptyResultsTextFrame.size) + transition.setPosition(view: view, position: emptyResultsTextFrame.center) + } + } else { + if let view = self.emptyResultsAnimation.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + if let view = self.emptyResultsTitle.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + if let view = self.emptyResultsText.view { + fadeTransition.setAlpha(view: view, alpha: 0.0, completion: { _ in + view.removeFromSuperview() + }) + } + } + return availableSize } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift index fd85a70f3d..0facc485d0 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAnimatedStickerItemNode/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1156,6 +1156,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { context: item.context, controllerInteraction: item.controllerInteraction, type: .standalone, + peer: nil, threadId: item.message.threadId ?? 1, parentMessage: item.message, constrainedSize: CGSize(width: availableContentWidth, height: CGFloat.greatestFiniteMagnitude), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index b47316f0f0..4b3582b94a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -1473,6 +1473,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI ignoreNameHiding = true } + if let subject = item.associatedData.subject, case let .customChatContents(contents) = subject, case .hashTagSearch = contents.kind { + ignoreNameHiding = true + } + displayAuthorInfo = !mergedTop.merged && allowAuthor && peerId.isGroupOrChannel && effectiveAuthor != nil if let forwardInfo = firstMessage.forwardInfo, forwardInfo.psaType != nil { displayAuthorInfo = false @@ -1500,6 +1504,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } else if incoming { hasAvatar = true } + + if let subject = item.associatedData.subject, case let .customChatContents(contents) = subject, case .hashTagSearch = contents.kind { + hasAvatar = true + } } var isInstantVideo = false @@ -2090,6 +2098,13 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if !displayHeader, case .peer = item.chatLocation, let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { displayHeader = true } + if case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind, let peer = item.message.peers[item.message.id.peerId] { + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + + } else { + displayHeader = true + } + } } let firstNodeTopPosition: ChatMessageBubbleRelativePosition @@ -2412,9 +2427,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI headerSize.height += 2.0 } } + + var hasThreadInfo = false + if case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1 || item.associatedData.isRecentActions), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { + hasThreadInfo = true + } else if case let .customChatContents(contents) = item.associatedData.subject, case .hashTagSearch = contents.kind { + hasThreadInfo = true + } var hasReply = replyMessage != nil || replyForward != nil || replyStory != nil - if !isInstantVideo, case let .peer(peerId) = item.chatLocation, (peerId == replyMessage?.id.peerId || item.message.threadId == 1 || item.associatedData.isRecentActions), let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum), item.message.associatedThreadInfo != nil { + if !isInstantVideo, hasThreadInfo { if let threadId = item.message.threadId, let replyMessage = replyMessage, Int64(replyMessage.id.id) == threadId { hasReply = false } @@ -2431,6 +2453,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI context: item.context, controllerInteraction: item.controllerInteraction, type: .bubble(incoming: incoming), + peer: item.message.peers[item.message.id.peerId].flatMap(EnginePeer.init), threadId: item.message.threadId ?? 1, parentMessage: item.message, constrainedSize: CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift index 516a8755eb..17108c48db 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode/Sources/ChatMessageInteractiveMediaNode.swift @@ -200,9 +200,22 @@ extension UIBezierPath { } private class ExtendedMediaOverlayNode: ASDisplayNode { + enum Icon { + case lock + case eye + + var image: UIImage { + switch self { + case .lock: + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white)! + case .eye: + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/AgeRestricted"), color: .white)! + } + } + } private let blurredImageNode: TransformImageNode private let dustNode: MediaDustNode - private let buttonNode: HighlightTrackingButtonNode + fileprivate let buttonNode: HighlightTrackingButtonNode private let highlightedBackgroundNode: ASDisplayNode private let iconNode: ASImageNode private let textNode: ImmediateTextNode @@ -214,7 +227,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { var isRevealed = false var tapped: () -> Void = {} - init(hasImageOverlay: Bool, enableAnimations: Bool) { + init(hasImageOverlay: Bool, icon: Icon, enableAnimations: Bool) { self.blurredImageNode = TransformImageNode() self.blurredImageNode.contentAnimations = [] @@ -231,10 +244,11 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false - self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: .white) + self.iconNode.image = icon.image self.textNode = ImmediateTextNode() - + self.textNode.isUserInteractionEnabled = false + super.init() if hasImageOverlay { @@ -244,8 +258,8 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.addSubnode(self.buttonNode) self.buttonNode.addSubnode(self.highlightedBackgroundNode) - self.addSubnode(self.iconNode) - self.addSubnode(self.textNode) + self.buttonNode.addSubnode(self.iconNode) + self.buttonNode.addSubnode(self.textNode) self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { @@ -263,7 +277,7 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { } @objc private func buttonPressed() { - + self.tapped() } override func didLoad() { @@ -284,10 +298,14 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.maskLayer = maskLayer } - func reveal() { + func reveal(animated: Bool = false) { self.isRevealed = true - self.blurredImageNode.removeFromSupernode() - self.dustNode.removeFromSupernode() + if animated { + self.dustNode.tap(at: CGPoint(x: self.dustNode.bounds.width / 2.0, y: self.dustNode.bounds.height / 2.0)) + } else { + self.blurredImageNode.removeFromSupernode() + self.dustNode.removeFromSupernode() + } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -317,12 +335,20 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.isRevealed = self.dustNode.isRevealed self.dustNode.revealed = { [weak self] in - self?.isRevealed = true - self?.blurredImageNode.removeFromSupernode() + guard let self else { + return + } + self.isRevealed = true + self.blurredImageNode.removeFromSupernode() + self.buttonNode.removeFromSupernode() } self.dustNode.tapped = { [weak self] in - self?.isRevealed = true - self?.tapped() + guard let self else { + return + } + if !self.isRevealed { + self.tapped() + } } } else { self.blurredImageNode.isHidden = true @@ -347,8 +373,8 @@ private class ExtendedMediaOverlayNode: ASDisplayNode { self.buttonNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - contentSize.width) / 2.0), y: floorToScreenPixels((size.height - contentSize.height) / 2.0)), size: contentSize) self.highlightedBackgroundNode.frame = CGRect(origin: .zero, size: contentSize) - self.iconNode.frame = CGRect(origin: CGPoint(x: self.buttonNode.frame.minX + padding, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) - self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: self.buttonNode.frame.minY + floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) + self.iconNode.frame = CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((contentSize.height - iconSize.height) / 2.0) + 1.0 - UIScreenPixel), size: iconSize) + self.textNode.frame = CGRect(origin: CGPoint(x: self.iconNode.frame.maxX + spacing, y: floorToScreenPixels((contentSize.height - textSize.height) / 2.0)), size: textSize) } } @@ -457,7 +483,8 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr public var activatePinch: ((PinchSourceContainerNode) -> Void)? public var updateMessageReaction: ((Message, ChatControllerInteractionReaction, Bool, ContextExtractedContentContainingView?) -> Void)? public var playMessageEffect: ((Message) -> Void)? - + public var activateAgeRestrictedMedia: (() -> Void)? + override public init() { self.pinchContainerNode = PinchSourceContainerNode() @@ -1867,7 +1894,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr backgroundColor = messageTheme.mediaDateAndStatusFillColor } - if let invoice = invoice { + if let invoice = invoice, invoice.currency != "XTR" { if let extendedMedia = invoice.extendedMedia { if case let .preview(_, _, maybeVideoDuration) = extendedMedia, let videoDuration = maybeVideoDuration { badgeContent = .text(inset: 0.0, backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, text: NSAttributedString(string: stringForDuration(videoDuration, position: nil)), iconName: nil) @@ -2216,6 +2243,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr badgeNode.removeFromSupernode() } + var icon: ExtendedMediaOverlayNode.Icon = .lock var displaySpoiler = false if let invoice = invoice, let extendedMedia = invoice.extendedMedia, case .preview = extendedMedia { displaySpoiler = true @@ -2223,14 +2251,26 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr displaySpoiler = true } else if isSecretMedia { displaySpoiler = true + } else if message.isAgeRestricted() { + displaySpoiler = true + icon = .eye } - + if displaySpoiler { if self.extendedMediaOverlayNode == nil { - let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, enableAnimations: (self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) && !isPreview) + let enableAnimations = (self.context?.sharedContext.energyUsageSettings.fullTranslucency ?? true) && !isPreview + let extendedMediaOverlayNode = ExtendedMediaOverlayNode(hasImageOverlay: !isSecretMedia, icon: icon, enableAnimations: enableAnimations) extendedMediaOverlayNode.tapped = { [weak self] in - self?.internallyVisible = true - self?.updateVisibility() + guard let self else { + return + } + if message.isAgeRestricted() { + self.activateAgeRestrictedMedia?() + } else { + self.internallyVisible = true + self.extendedMediaOverlayNode?.isRevealed = true + self.updateVisibility() + } } self.extendedMediaOverlayNode = extendedMediaOverlayNode self.pinchContainerNode.contentNode.insertSubnode(extendedMediaOverlayNode, aboveSubnode: self.imageNode) @@ -2249,21 +2289,26 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr self.extendedMediaOverlayNode?.isUserInteractionEnabled = tappable - var paymentText: String = "" - outer: for attribute in message.attributes { - if let attribute = attribute as? ReplyMarkupMessageAttribute { - for row in attribute.rows { - for button in row.buttons { - if case .payment = button.action { - paymentText = button.title - break outer + var viewText: String = "" + if message.isAgeRestricted() { + //TODO: localize + viewText = "18+ Content" + } else { + outer: for attribute in message.attributes { + if let attribute = attribute as? ReplyMarkupMessageAttribute { + for row in attribute.rows { + for button in row.buttons { + if case .payment = button.action { + viewText = button.title + break outer + } } } + break } - break } } - self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: paymentText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners) + self.extendedMediaOverlayNode?.update(size: self.imageNode.frame.size, text: viewText, imageSignal: self.currentBlurredImageSignal, imageFrame: self.imageNode.view.convert(self.imageNode.bounds, to: self.extendedMediaOverlayNode?.view), corners: self.currentImageArguments?.corners) } else if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { self.extendedMediaOverlayNode = nil extendedMediaOverlayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak extendedMediaOverlayNode] _ in @@ -2286,6 +2331,10 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr } } + public func reveal() { + self.extendedMediaOverlayNode?.reveal(animated: true) + } + public static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ dateTimeFormat: PresentationDateTimeFormat, _ message: Message, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ media: Media, _ dateAndStatus: ChatMessageDateAndStatus?, _ automaticDownload: InteractiveMediaNodeAutodownloadMode, _ peerType: MediaAutoDownloadPeerType, _ peerId: EnginePeer.Id?, _ sizeCalculation: InteractiveMediaNodeSizeCalculation, _ layoutConstants: ChatMessageItemLayoutConstants, _ contentMode: InteractiveMediaNodeContentMode, _ presentationContext: ChatPresentationContext) -> (CGSize, CGFloat, (CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() @@ -2465,4 +2514,14 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr return nil } } + + public func ignoreTapActionAtPoint(_ point: CGPoint) -> Bool { + if let extendedMediaOverlayNode = self.extendedMediaOverlayNode { + let convertedPoint = self.view.convert(point, to: extendedMediaOverlayNode.view) + if extendedMediaOverlayNode.buttonNode.frame.contains(convertedPoint) { + return true + } + } + return false + } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift index 168f20a5f2..011839327a 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageMediaBubbleContentNode/Sources/ChatMessageMediaBubbleContentNode.swift @@ -42,20 +42,28 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { self.addSubnode(self.interactiveImageNode) self.interactiveImageNode.activateLocalContent = { [weak self] mode in - if let strongSelf = self { - if let item = strongSelf.item { - let openChatMessageMode: ChatControllerInteractionOpenMessageMode - switch mode { - case .default: - openChatMessageMode = .default - case .stream: - openChatMessageMode = .stream - case .automaticPlayback: - openChatMessageMode = .automaticPlayback - } - let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode)) - } + guard let self, let item = self.item else { + return } + let openChatMessageMode: ChatControllerInteractionOpenMessageMode + switch mode { + case .default: + openChatMessageMode = .default + case .stream: + openChatMessageMode = .stream + case .automaticPlayback: + openChatMessageMode = .automaticPlayback + } + let _ = item.controllerInteraction.openMessage(item.message, OpenMessageParams(mode: openChatMessageMode)) + } + + self.interactiveImageNode.activateAgeRestrictedMedia = { [weak self] in + guard let self, let item = self.item else { + return + } + let _ = item.controllerInteraction.openAgeRestrictedMessageMedia(item.message, { [weak self] in + self?.interactiveImageNode.reveal() + }) } self.interactiveImageNode.updateMessageReaction = { [weak self] message, value, force, sourceView in @@ -470,6 +478,9 @@ public class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { + if self.interactiveImageNode.ignoreTapActionAtPoint(point) { + return ChatMessageBubbleContentTapAction(content: .ignore) + } return ChatMessageBubbleContentTapAction(content: .none) } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift index fef6916251..62e1b527e6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageStickerItemNode/Sources/ChatMessageStickerItemNode.swift @@ -731,6 +731,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView { context: item.context, controllerInteraction: item.controllerInteraction, type: .standalone, + peer: nil, threadId: item.message.threadId ?? 1, parentMessage: item.message, constrainedSize: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index c6ce0ca329..d28ddddc49 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -263,8 +263,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { default: break } - if case .customChatContents = item.associatedData.subject { - displayStatus = false + if case let .customChatContents(contents) = item.associatedData.subject { + if case .hashTagSearch = contents.kind { + displayStatus = true + } else { + displayStatus = false + } } if displayStatus { if incoming { diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD index a870c33413..6b225f349c 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD +++ b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/BUILD @@ -29,6 +29,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/WallpaperBackgroundNode", "//submodules/TelegramUI/Components/ChatControllerInteraction", + "//submodules/AvatarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift index f8f4523fd8..3e5efe2ef5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageThreadInfoNode/Sources/ChatMessageThreadInfoNode.swift @@ -19,6 +19,7 @@ import ComponentFlow import EmojiStatusComponent import WallpaperBackgroundNode import ChatControllerInteraction +import AvatarNode private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) { enum CornerType { @@ -184,7 +185,8 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { public let context: AccountContext public let controllerInteraction: ChatControllerInteraction public let type: ChatMessageThreadInfoType - public let threadId: Int64 + public let peer: EnginePeer? + public let threadId: Int64? public let parentMessage: Message public let constrainedSize: CGSize public let animationCache: AnimationCache? @@ -196,7 +198,8 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { context: AccountContext, controllerInteraction: ChatControllerInteraction, type: ChatMessageThreadInfoType, - threadId: Int64, + peer: EnginePeer?, + threadId: Int64?, parentMessage: Message, constrainedSize: CGSize, animationCache: AnimationCache?, @@ -207,6 +210,7 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { self.context = context self.controllerInteraction = controllerInteraction self.type = type + self.peer = peer self.threadId = threadId self.parentMessage = parentMessage self.constrainedSize = constrainedSize @@ -239,6 +243,7 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { private let contentBackgroundNode: ASImageNode private var textNode: TextNodeWithEntities? private let arrowNode: ASImageNode + private var avatarNode: AvatarNode? private var titleTopicIconView: ComponentHostView? private var titleTopicIconComponent: EmojiStatusComponent? @@ -322,6 +327,8 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { topicTitle = threadInfo.title topicIconId = threadInfo.icon topicIconColor = threadInfo.iconColor + } else if let peer = arguments.peer { + topicTitle = peer.displayTitle(strings: arguments.presentationData.strings, displayOrder: arguments.presentationData.nameDisplayOrder) } let backgroundColor: UIColor @@ -362,7 +369,10 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { let fillInset: CGFloat = 5.0 let iconSize = CGSize(width: 22.0, height: 22.0) let insets = UIEdgeInsets(top: 2.0, left: 4.0, bottom: 2.0, right: 4.0) - let spacing: CGFloat = 4.0 + var spacing: CGFloat = 4.0 + if arguments.peer != nil { + spacing += 3.0 + } let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: arguments.constrainedSize.width - insets.left - insets.right - iconSize.width - spacing, height: arguments.constrainedSize.height), alignment: .natural, cutout: nil, insets: .zero)) @@ -393,7 +403,11 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { } node.pressed = { - arguments.controllerInteraction.navigateToThreadMessage(arguments.parentMessage.id.peerId, arguments.threadId, arguments.parentMessage.id) + if let _ = arguments.peer { + arguments.controllerInteraction.navigateToMessage(arguments.parentMessage.id, arguments.parentMessage.id, NavigateToMessageParams(timestamp: nil, quote: nil, forceNew: true)) + } else if let threadId = arguments.threadId { + arguments.controllerInteraction.navigateToThreadMessage(arguments.parentMessage.id.peerId, threadId, arguments.parentMessage.id) + } } if node.lineRects != lineRects { @@ -471,57 +485,69 @@ public class ChatMessageThreadInfoNode: ASDisplayNode { node.contentNode.addSubnode(textNode.textNode) } - let titleTopicIconView: ComponentHostView - if let current = node.titleTopicIconView { - titleTopicIconView = current - } else { - titleTopicIconView = ComponentHostView() - node.titleTopicIconView = titleTopicIconView - node.contentNode.view.addSubview(titleTopicIconView) - } - - let titleTopicIconContent: EmojiStatusComponent.Content - var containerSize: CGSize = CGSize(width: 22.0, height: 22.0) - var iconX: CGFloat = 0.0 - if arguments.threadId == 1 { - titleTopicIconContent = .image(image: generalThreadIcon) - containerSize = CGSize(width: 18.0, height: 18.0) - iconX = 3.0 - } else if let fileId = topicIconId, fileId != 0 { - titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: arguments.presentationData.theme.theme.list.mediaPlaceholderColor, themeColor: arguments.presentationData.theme.theme.list.itemAccentColor, loopMode: .count(1)) - } else { - titleTopicIconContent = .topic(title: String(topicTitle.prefix(1)), color: topicIconColor, size: CGSize(width: 22.0, height: 22.0)) - } - - if let animationCache = arguments.animationCache, let animationRenderer = arguments.animationRenderer { - let titleTopicIconComponent = EmojiStatusComponent( - context: arguments.context, - animationCache: animationCache, - animationRenderer: animationRenderer, - content: titleTopicIconContent, - isVisibleForAnimations: node.visibility, - action: nil - ) - node.titleTopicIconComponent = titleTopicIconComponent - - let iconSize = titleTopicIconView.update( - transition: .immediate, - component: AnyComponent(titleTopicIconComponent), - environment: {}, - containerSize: containerSize - ) - - let iconY: CGFloat - if let firstLineMidY = firstLineMidY { - iconY = floorToScreenPixels(firstLineMidY - iconSize.height / 2.0) + if let peer = arguments.peer { + let avatarNode: AvatarNode + if let current = node.avatarNode { + avatarNode = current } else { - iconY = 0.0 + avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 15.0)) + node.contentNode.addSubnode(avatarNode) + } + avatarNode.frame = CGRect(origin: CGPoint(x: -1.0, y: -3.0), size: CGSize(width: 26.0, height: 26.0)) + avatarNode.setPeer(context: arguments.context, theme: arguments.presentationData.theme.theme, peer: peer) + } else { + let titleTopicIconView: ComponentHostView + if let current = node.titleTopicIconView { + titleTopicIconView = current + } else { + titleTopicIconView = ComponentHostView() + node.titleTopicIconView = titleTopicIconView + node.contentNode.view.addSubview(titleTopicIconView) } - titleTopicIconView.frame = CGRect(origin: CGPoint(x: insets.left + iconX, y: insets.top + iconY), size: iconSize) + let titleTopicIconContent: EmojiStatusComponent.Content + var containerSize: CGSize = CGSize(width: 22.0, height: 22.0) + var iconX: CGFloat = 0.0 + if arguments.threadId == 1 { + titleTopicIconContent = .image(image: generalThreadIcon) + containerSize = CGSize(width: 18.0, height: 18.0) + iconX = 3.0 + } else if let fileId = topicIconId, fileId != 0 { + titleTopicIconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 36.0, height: 36.0), placeholderColor: arguments.presentationData.theme.theme.list.mediaPlaceholderColor, themeColor: arguments.presentationData.theme.theme.list.itemAccentColor, loopMode: .count(1)) + } else { + titleTopicIconContent = .topic(title: String(topicTitle.prefix(1)), color: topicIconColor, size: CGSize(width: 22.0, height: 22.0)) + } + + if let animationCache = arguments.animationCache, let animationRenderer = arguments.animationRenderer { + let titleTopicIconComponent = EmojiStatusComponent( + context: arguments.context, + animationCache: animationCache, + animationRenderer: animationRenderer, + content: titleTopicIconContent, + isVisibleForAnimations: node.visibility, + action: nil + ) + node.titleTopicIconComponent = titleTopicIconComponent + + let iconSize = titleTopicIconView.update( + transition: .immediate, + component: AnyComponent(titleTopicIconComponent), + environment: {}, + containerSize: containerSize + ) + + let iconY: CGFloat + if let firstLineMidY = firstLineMidY { + iconY = floorToScreenPixels(firstLineMidY - iconSize.height / 2.0) + } else { + iconY = 0.0 + } + + titleTopicIconView.frame = CGRect(origin: CGPoint(x: insets.left + iconX, y: insets.top + iconY), size: iconSize) + } } - let textFrame = CGRect(origin: CGPoint(x: iconSize.width + 2.0 + insets.left, y: insets.top), size: textLayout.size) + let textFrame = CGRect(origin: CGPoint(x: iconSize.width + (spacing - 2.0) + insets.left, y: insets.top), size: textLayout.size) textNode.textNode.frame = textFrame if let arrowIcon = arrowIcon, let firstLine = lineRects.first, let lastLine = lineRects.last { diff --git a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift index 023ad26a55..869ea6242a 100644 --- a/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift +++ b/submodules/TelegramUI/Components/Chat/FactCheckAlertController/Sources/FactCheckAlertController.swift @@ -219,7 +219,7 @@ private final class FactCheckAlertContentNode: AlertContentNode { customInputView: nil, resetText: nil, isOneLineWhenUnfocused: false, - characterLimit: nil, + characterLimit: 1024, emptyLineHandling: .oneConsecutive, formatMenuAvailability: .available([.bold, .italic, .link]), returnKeyType: .done, diff --git a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift index 12024e3f88..ebf1719ee4 100644 --- a/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Components/ChatControllerInteraction/Sources/ChatControllerInteraction.swift @@ -97,11 +97,13 @@ public struct NavigateToMessageParams { public var timestamp: Double? public var quote: Quote? public var progress: Promise? + public var forceNew: Bool - public init(timestamp: Double?, quote: Quote?, progress: Promise? = nil) { + public init(timestamp: Double?, quote: Quote?, progress: Promise? = nil, forceNew: Bool = false) { self.timestamp = timestamp self.quote = quote self.progress = progress + self.forceNew = forceNew } } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index fa47f65448..56612ab51f 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -1828,6 +1828,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode { switch customChatContents.kind { case .quickReplyMessageInput: break + case .hashTagSearch: + break case .businessLinkSetup: stickerContent = nil gifContent = nil diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 2779b41157..b280a5231f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -249,6 +249,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { fileType = .sticker } self.content = .file(.standalone(media: file), fileType) + } else if let dataPath = try container.decodeIfPresent(String.self, forKey: .animatedImagePath), let data = try? Data(contentsOf: URL(fileURLWithPath: fullEntityMediaPath(dataPath))), let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let thumbnailImage = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { + self.content = .animatedImage(data, thumbnailImage) } else if let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { let isRectangle = try container.decodeIfPresent(Bool.self, forKey: .isRectangle) ?? false let isDualPhoto = try container.decodeIfPresent(Bool.self, forKey: .isDualPhoto) ?? false @@ -261,8 +263,6 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { imageType = .sticker } self.content = .image(image, imageType) - } else if let dataPath = try container.decodeIfPresent(String.self, forKey: .animatedImagePath), let data = try? Data(contentsOf: URL(fileURLWithPath: fullEntityMediaPath(dataPath))), let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let thumbnailImage = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { - self.content = .animatedImage(data, thumbnailImage) } else if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .videoFile) { self.content = .video(file) } else { @@ -407,3 +407,61 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return true } } + +public extension UIImage { + class func animatedImageFromData(data: Data) -> DrawingAnimatedImage? { + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { + return nil + } + + let count = CGImageSourceGetCount(source) + var images = [UIImage]() + var duration = 0.0 + + for i in 0.. Double { + var delay = 0.0 + guard #available(iOS 13.0, *) else { + return delay + } + + let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) + let gifPropertiesPointer = UnsafeMutablePointer.allocate(capacity: 0) + if CFDictionaryGetValueIfPresent(cfProperties, Unmanaged.passUnretained(kCGImagePropertyHEICSDictionary).toOpaque(), gifPropertiesPointer) == false { + return delay + } + + let gifProperties:CFDictionary = unsafeBitCast(gifPropertiesPointer.pointee, to: CFDictionary.self) + + var delayObject: AnyObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyHEICSUnclampedDelayTime).toOpaque()), to: AnyObject.self) + if delayObject.doubleValue == 0 { + delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties, Unmanaged.passUnretained(kCGImagePropertyHEICSDelayTime).toOpaque()), to: AnyObject.self) + } + + delay = delayObject as? Double ?? 0 + + return delay + } +} + +public final class DrawingAnimatedImage { + public let images: [UIImage] + public let duration: Double + + init(images: [UIImage], duration: Double) { + self.images = images + self.duration = duration + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index c41ecc71a7..99dc63546e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -67,14 +67,19 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return [] } else { let content: MediaEditorComposerStickerEntity.Content + var scale = entity.scale switch entity.content { case let .file(file, _): content = .file(file.media) case let .image(image, _): content = .image(image) case let .animatedImage(data, _): - let _ = data - return [] + if let animatedImage = UIImage.animatedImageFromData(data: data) { + content = .animatedImage(animatedImage.images, animatedImage.duration) + scale *= 1.0 + } else { + return [] + } case let .video(file): content = .video(file) case .dualVideoReference: @@ -93,7 +98,7 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return [] } } - return [MediaEditorComposerStickerEntity(postbox: postbox, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace, tintColor: tintColor, isStatic: entity.isExplicitlyStatic)] + return [MediaEditorComposerStickerEntity(postbox: postbox, content: content, position: entity.position, scale: scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace, tintColor: tintColor, isStatic: entity.isExplicitlyStatic)] } } else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { if let entity = entity as? DrawingBubbleEntity { @@ -296,8 +301,9 @@ final class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { self.imagePromise.set(.single(image)) case let .animatedImage(images, duration): self.isAnimated = true - let _ = images - let _ = duration + self.videoFrameRate = Float(images.count) / Float(duration) + self.totalDuration = duration + self.durationPromise.set(.single(duration)) case .video: self.isAnimated = true } @@ -338,7 +344,37 @@ final class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { func image(for time: CMTime, frameRate: Float, context: CIContext, completion: @escaping (CIImage?) -> Void) { let currentTime = CMTimeGetSeconds(time) - if case .video = self.content { + if case let .animatedImage(images, _) = self.content { + var frameAdvancement: Int = 0 + if let frameRate = self.videoFrameRate, frameRate > 0 { + let frameTime = 1.0 / Double(frameRate) + let frameIndex = Int(floor(currentTime / frameTime)) + + let currentFrameIndex = self.currentFrameIndex + if currentFrameIndex != frameIndex { + let previousFrameIndex = currentFrameIndex + self.currentFrameIndex = frameIndex + + var delta = 1 + if let previousFrameIndex = previousFrameIndex { + delta = max(1, frameIndex - previousFrameIndex) + } + frameAdvancement = delta + } + } + if frameAdvancement == 0, let image = self.image { + completion(image) + return + } else if let currentFrameIndex = self.currentFrameIndex { + let index = currentFrameIndex % images.count + var image = images[index] + image = generateScaledImage(image: images[index], size: image.size.aspectFitted(CGSize(width: 384, height: 384)), opaque: false, scale: 1.0)! + let ciImage = CIImage(image: image) + self.image = ciImage + completion(ciImage) + return + } + } else if case .video = self.content { if self.videoOutput == nil { self.setupVideoOutput() } @@ -475,7 +511,6 @@ final class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let options = NSMutableDictionary() options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString) - var pixelBuffer: CVPixelBuffer? CVPixelBufferCreate( kCFAllocatorDefault, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift index 0c44d4b0d3..c3333137e2 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoChatListPaneNode/Sources/PeerInfoChatListPaneNode.swift @@ -402,7 +402,7 @@ public final class PeerInfoChatListPaneNode: ASDisplayNode, PeerInfoPaneNode, AS if self.chatController == nil { let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: self.context.account.peerId), subject: nil, botStart: nil, mode: .standard(.embedded(invertDirection: false))) chatController.alwaysShowSearchResultsAsList = true - + chatController.includeSavedPeersInSearchResults = true self.chatController = chatController chatController.navigation_setNavigationController(self.navigationController()) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift index aa8e3b2029..d9e9c17fe1 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift @@ -800,6 +800,12 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, let starsState: Signal if let starsContext { starsState = starsContext.state + |> map { state in + if let state, state.balance > 0 || !state.transactions.isEmpty { + return state + } + return nil + } } else { starsState = .single(nil) } @@ -875,7 +881,8 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, bots: bots, hasPassport: hasPassport, hasWatchApp: hasWatchApp, - enableQRLogin: enableQRLogin) + enableQRLogin: enableQRLogin + ) return PeerInfoScreenData( peer: peer, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index dab75b4fc0..b8becd4489 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -979,16 +979,17 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 100, label: .text(""), text: presentationData.strings.Settings_Premium, icon: PresentationResourcesSettings.premium, action: { interaction.openSettings(.premium) })) - //TODO:localize - let balanceText: String - if let balance = data.starsState?.balance, balance > 0 { - balanceText = "\(balance)" - } else { - balanceText = "" + if let starsState = data.starsState { + let balanceText: String + if starsState.balance > 0 { + balanceText = "\(starsState.balance)" + } else { + balanceText = "" + } + items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(balanceText), text: presentationData.strings.Settings_Stars, icon: PresentationResourcesSettings.stars, action: { + interaction.openSettings(.stars) + })) } - items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 102, label: .text(balanceText), text: "Your Stars", icon: PresentationResourcesSettings.stars, action: { - interaction.openSettings(.stars) - })) items[.payment]!.append(PeerInfoScreenDisclosureItem(id: 103, label: .text(""), additionalBadgeLabel: presentationData.strings.Settings_New, text: presentationData.strings.Settings_Business, icon: PresentationResourcesSettings.business, action: { interaction.openSettings(.businessSetup) })) diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift index cde85a0476..66e706dddf 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupChatContents.swift @@ -215,6 +215,8 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco initialShortcut = shortcut case .businessLinkSetup: initialShortcut = "" + case .hashTagSearch: + initialShortcut = "" } let queue = Queue() @@ -251,9 +253,19 @@ final class AutomaticBusinessMessageSetupChatContents: ChatCustomContentsProtoco } case .businessLinkSetup: break + case .hashTagSearch: + break } } func businessLinkUpdate(message: String, entities: [MessageTextEntity], title: String?) { } + + func loadMore() { + } + + func hashtagSearchUpdate(query: String) { + } + + var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void = { _ in } } diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkChatContents.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkChatContents.swift index 3b69806c61..13f07d581f 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkChatContents.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/BusinessLinkChatContents.swift @@ -85,4 +85,12 @@ final class BusinessLinkChatContents: ChatCustomContentsProtocol { )) } } + + func loadMore() { + } + + func hashtagSearchUpdate(query: String) { + } + + var hashtagSearchResultsUpdate: ((SearchMessagesResult, SearchMessagesState)) -> Void = { _ in } } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/Contents.json new file mode 100644 index 0000000000..0c68b117b1 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "clearrecents_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/clearrecents_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/clearrecents_30.pdf new file mode 100644 index 0000000000..cf8a1640fc --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/ClearRecent.imageset/clearrecents_30.pdf @@ -0,0 +1,62 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Filter /FlateDecode + /Length 3 0 R + >> +stream +xeˎ0 yI'vxjH3#x~4.+}|?o_>>>}OxKS`g(gؑTŬY:h7tj0 ;_HёnWvM  DfVۣ *YkE:b\q}V?6sL +< +Yf99PR@nL5i{2D ??C&$ ,֊ 7hCNQZkT`c4KQ +T!PBk> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000702 00000 n +0000000724 00000 n +0000000897 00000 n +0000000971 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1030 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/EmptyHashtag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/EmptyHashtag.imageset/Contents.json new file mode 100644 index 0000000000..19cb866d35 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/EmptyHashtag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tagempty_80.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/EmptyHashtag.imageset/tagempty_80.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/EmptyHashtag.imageset/tagempty_80.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ff7a6c11ef2198a62441bf40f7ce5bcc3329c1eb GIT binary patch literal 1377 zcmY!laBB55sH4d%$$uUS(@j+?)%~2m9yS^bno7NZsr;n zW5ILNrR{v&1-bPT*L!k5 zzH*z>E3kLjMh89F{kvsdlQX%U*6bEF@$)#UazxdB&(+=AkFOPWTp;TFr%_*RubNYp z%hsr6aT1IsQX(@Y&U0S7WF;B&v0<}Xhg@gqch<<4yRWj&Z+dWZ)!m()2j9Kp%_`pw z(r=daboT+z^(VU(7z$H5cSXK9wqo%NgCpKur*3*@Ut9aoX!ANj$)(u~tx~#r%OAIO zm2fWX+B%`}5c93eyH_%phW7Dp+-){z!Uwr~$CpQK*{)Y5Wd7;&!Hg|ai`Y~1H)XNq z_$V6e?M^$=X!iBu$M&oR3aMvKD9vVGdpof5fU}vNndAGO4}3QrUPSV#d{vb873<0W zqarZ5<;sBpKb0g(~}o0H7~u^=-Zrk z@T^;y{@j~C?%b0)k=T2=MB-I##G=RF6r*k!<#NwW-dQo{^6opfl2@t=Zo_U%mQ++?=GS_5xecCc&< z!;OtQ7ihRJy*19;nXl)#_e#^BCBo5sD+8AaA2qApT)zHgxKhuoch`*1>nA@oc>j3g zHU7e{wKid^So_RHFEfYyIsSgf`{TR6%-XlCeww%^D=3*m(ONteuW1)#Zq_QAYK|dfd9hMt>Q&Tb%o$@ObqJdcpoU;rN3=6mrf(0`v zD784hv?w{XSOJuAK{*MOxt#O!N>cNHHiB{^OfVp^2q>mt3Kat8IG7heLWn%?otXl3 zsshLdK?*Q?obyY8dJQq$Uku5+Fq6zsOaghZ7;YiRVGxfyCzd4UK^(P(I333NVG zsH7+{Gbgo(3lw{vF2LZ>D9+DK)l|^POwoh{ihfXjeu)Ce!{7kc56-Mg1-c$wkR=wC RfW2dAXllx(s_N?R1^{FL^!We) literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/RecentHashtag.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/RecentHashtag.imageset/Contents.json new file mode 100644 index 0000000000..dc214f3c0e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/RecentHashtag.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "tagsearch_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/RecentHashtag.imageset/tagsearch_30.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Hashtag/RecentHashtag.imageset/tagsearch_30.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4854fca9ab6f39b7b4e9ac1478bb2a0349b89963 GIT binary patch literal 1317 zcmY!laBvK;I`dFTEr~!5FAK2q*+Jp}3?dH8Gc~f-!Z<>AWir0%;r=-#$FP{rv0c^G`4TeE-{~ zKd~#m{@woV_ru3FXY&_lev#>OmZ(0p%U1XEt>1@Ff7|1-+0Sv$B=eVoF0X?R9=XXq zi&rUf@k;H(=B}@EQkXkV6niKxI(X{!wu;ZcI6^P(SUq9=y)EA-2HrEebEsj7&bJk6 zDuK(cd9-D%JED8D`pK`3_Ud-)T4vWtPq`+tEPXt$@RY+2?xe@NwjTV#x6(B$VCKEO zfzp)~yWQtzL{GSSF7lUJP0SYF`wLA!H-GhtQvD(NxJ2CQrcSe|<64CuPFAv2>e6RR zrd!+gs#R53J@Vb;D4xAO#qZk=59J*a%xsyjmRP?|-tpeAN<+#~%33>g{)SxHMx%!0&8KYDhU?{x?`5+XUbNwDSas^`m+6olpD?(v;;IF}Z+EZF#$ zy0v>x_^%6xo!Y+dYnwJPNuA}C$>&w)0uKtua@DrF+cU`COS{Y<8>rr->80kpAm7Qu z@mp?t#pRrEpQQ`RKRhwNvO0RgE>F$q=?eGLAJ}QC@9w)Y^FC9;0m8*`;q!s`dMhPr|L>{M)O<&hDG#VErq{ z;rSikj=s1BeRfX&@=vdf5Z|F?wq1DAnw+ak!sp&|+rF!yHDZ2#`K!mDq{}~kTd~{t zr2mZeE3$Fk?JbK#1zvF(uH5NdV|PaTpY?pvSJ&I0R{q@ghkeli+YD@4aa6M9HxL8^j&Kw>&9<@=_lWF|W0S13d)80Z;* z0fJ!+7ecULCIzJy=a&{Grxq)KG9M_{fHIPEeqKpx9?(Wm&VvaCBo+b16ilH)Ncj^a z1kbnn-kB*trz(Ja5TpRJ$2q?gsMip~{l$=c3JVM~6q7(6EQVVMau~$p&WR<7Ir-_R zS}TfD)3^*2%()=m2ayV9rl!WG3P52fFf>pAvlQ~+LdHOMf&fCu+{_$9$ifm`$kGfL zHYgem4UB=#hYFPxC1&QN7IA@M&(j4M92&*>xv8288ks4YkU-H7%Fi!R0C^Z3!1}?N aRjEMNgA1+1q7txo3=J$zxl~nM{oMdv2kU_V literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index 6b8f550ef3..ed51dee3b1 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1269,11 +1269,14 @@ extension ChatControllerImpl { donateSendMessageIntent(account: strongSelf.context.account, sharedContext: strongSelf.context.sharedContext, intentContext: .chat, peerIds: [peerId]) } else if case let .customChatContents(customChatContents) = strongSelf.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: customChatContents.enqueueMessages(messages: messages) strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory() case let .businessLinkSetup(link): if messages.count > 1 { + //TODO:localize strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: "The message text limit is 4096 characters", actions: [ TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {}) ]), in: .window(.root)) @@ -1306,6 +1309,40 @@ extension ChatControllerImpl { strongSelf.updateChatPresentationInterfaceState(interactive: true, { $0.updatedShowCommands(false) }) } + if case let .customChatContents(customChatContents) = self.subject { + customChatContents.hashtagSearchResultsUpdate = { [weak self] searchResult in + guard let self else { + return + } + let (results, state) = searchResult + let isEmpty = results.totalCount == 0 + if isEmpty { + self.alwaysShowSearchResultsAsList = true + } + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + var updatedState = current + if let data = current.search { + let messageIndices = results.messages.map({ $0.index }).sorted() + var currentIndex = messageIndices.last + if let previousResultId = data.resultsState?.currentId { + for index in messageIndices { + if index.id >= previousResultId { + currentIndex = index + break + } + } + } + updatedState = updatedState.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: state, totalCount: results.totalCount, completed: results.completed))) + } + if isEmpty { + updatedState = updatedState.updatedDisplayHistoryFilterAsList(true) + } + return updatedState + }) + self.searchResult.set(.single((results, state, .general(scope: .channels, tags: nil, minDate: nil, maxDate: nil)))) + } + } + self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] transition, saveInterfaceState, f in self?.updateChatPresentationInterfaceState(transition: transition, interactive: true, saveInterfaceState: saveInterfaceState, { $0.updatedInterfaceState(f) }) } @@ -1364,6 +1401,8 @@ extension ChatControllerImpl { self?.enqueueGifData(data) case let .sticker(image, isMemoji): self?.enqueueStickerImage(image, isMemoji: isMemoji) + case let .animatedSticker(data): + self?.enqueueAnimatedStickerData(data) } } self.chatDisplayNode.updateTypingActivity = { [weak self] value in @@ -1442,7 +1481,9 @@ extension ChatControllerImpl { return } - if let resultsState = self.presentationInterfaceState.search?.resultsState, !resultsState.messageIndices.isEmpty { + if case let .customChatContents(contents) = self.presentationInterfaceState.subject, case .hashTagSearch = contents.kind { + self.chatDisplayNode.historyNode.scrollToEndOfHistory() + } else if let resultsState = self.presentationInterfaceState.search?.resultsState, !resultsState.messageIndices.isEmpty { if let currentId = resultsState.currentId, let index = resultsState.messageIndices.firstIndex(where: { $0.id == currentId }) { if index != resultsState.messageIndices.count - 1 { self.interfaceInteraction?.navigateMessageSearch(.later) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index 092847181d..d53572aab0 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -29,7 +29,7 @@ extension ChatControllerImpl { guard let self else { return } - self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId) + self.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId && !params.forceNew, forceNew: params.forceNew) } let _ = (self.context.engine.data.get( @@ -72,6 +72,7 @@ extension ChatControllerImpl { scrollPosition: ListViewScrollPosition = .center(.bottom), rememberInStack: Bool = true, forceInCurrentChat: Bool = false, + forceNew: Bool = false, dropStack: Bool = false, animated: Bool = true, completion: (() -> Void)? = nil, @@ -104,11 +105,11 @@ extension ChatControllerImpl { if case let .peer(peerId) = self.chatLocation, messageLocation.peerId == peerId, !isPinnedMessages, !isScheduledMessages { forceInCurrentChat = true } - if case .customChatContents = self.chatLocation { + if case .customChatContents = self.chatLocation, !forceNew { forceInCurrentChat = true } - if isPinnedMessages, let messageId = messageLocation.messageId { + if isPinnedMessages || forceNew, let messageId = messageLocation.messageId { let _ = (combineLatest( self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)), self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local) diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index 09780c583b..71709a4294 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -223,6 +223,8 @@ func updateChatPresentationInterfaceStateImpl( var canHaveUrlPreview = true if case let .customChatContents(customChatContents) = updatedChatPresentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: diff --git a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift index fbe13ebdb1..b17db6edb0 100644 --- a/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatBusinessLinkTitlePanelNode.swift @@ -194,6 +194,8 @@ final class ChatBusinessLinkTitlePanelNode: ChatTitleAccessoryPanelNode { break case let .businessLinkSetup(link): self.link = link + case .hashTagSearch: + break } default: break diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 55f308f31c..6dcb92f230 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -113,6 +113,7 @@ import ChatNavigationButton import WebsiteType import ChatQrCodeScreen import PeerInfoScreen +import MediaEditor import MediaEditorScreen import WallpaperGalleryScreen import WallpaperGridScreen @@ -321,7 +322,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let editingMessage = ValuePromise(nil, ignoreRepeated: true) let startingBot = ValuePromise(false, ignoreRepeated: true) let unblockingPeer = ValuePromise(false, ignoreRepeated: true) - let searching = ValuePromise(false, ignoreRepeated: true) + public let searching = ValuePromise(false, ignoreRepeated: true) let searchResult = Promise<(SearchMessagesResult, SearchMessagesState, SearchMessagesLocation)?>() let loadingMessage = Promise(nil) let performingInlineSearch = ValuePromise(false, ignoreRepeated: true) @@ -596,6 +597,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var networkSpeedEventsDisposable: Disposable? + var stickerVideoExport: MediaEditorVideoExport? + var messageComposeController: MFMessageComposeViewController? public var alwaysShowSearchResultsAsList: Bool = false { @@ -605,6 +608,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + public var includeSavedPeersInSearchResults: Bool = false { + didSet { + self.chatDisplayNode.includeSavedPeersInSearchResults = self.includeSavedPeersInSearchResults + } + } + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(.default), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], customChatNavigationStack: [EnginePeer.Id]? = nil) { let _ = ChatControllerCount.modify { value in return value + 1 @@ -684,6 +693,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return interfaceState.withUpdatedEffectiveInputState(ChatTextInputState(inputText: chatInputStateStringWithAppliedEntities(link.message, entities: link.entities))) }) } + case .hashTagSearch: + break } } @@ -745,6 +756,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .customChatContents(customChatContents) = strongSelf.presentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + return true case let .quickReplyMessageInput(_, shortcutType): if let historyView = strongSelf.chatDisplayNode.historyNode.originalHistoryView, historyView.entries.isEmpty { @@ -6449,6 +6462,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if case let .customChatContents(customChatContents) = self.subject { switch customChatContents.kind { + case .hashTagSearch: + break case let .quickReplyMessageInput(shortcut, shortcutType): switch shortcutType { case .generic: @@ -9286,7 +9301,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } })) } - + func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { if !canSendMessagesToChat(self.presentationInterfaceState) { return @@ -9385,14 +9400,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G func updateDownButtonVisibility() { let recordingMediaMessage = self.audioRecorderValue != nil || self.videoRecorderValue != nil || self.presentationInterfaceState.interfaceState.mediaDraftState != nil - if let search = self.presentationInterfaceState.search, let results = search.resultsState, results.messageIndices.count != 0 { + var ignoreSearchState = false + if case let .customChatContents(contents) = self.subject, case .hashTagSearch = contents.kind { + ignoreSearchState = true + } + + if !ignoreSearchState, let search = self.presentationInterfaceState.search, let results = search.resultsState, results.messageIndices.count != 0 { var resultIndex: Int? if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) { resultIndex = index } else { resultIndex = nil } - if let resultIndex { self.chatDisplayNode.navigateButtons.directionButtonState = ChatHistoryNavigationButtons.DirectionState( up: ChatHistoryNavigationButtons.ButtonState(isEnabled: resultIndex != 0), @@ -9536,7 +9555,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var cancelImpl: (() -> Void)? let presentationData = self.presentationData let progressSignal = Signal { [weak self] subscriber in - let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { + let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) self?.present(controller, in: .window(.root)) @@ -9547,7 +9566,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } |> runOn(Queue.mainQueue()) - |> delay(0.15, queue: Queue.mainQueue()) + |> delay(0.25, queue: Queue.mainQueue()) let progressDisposable = progressSignal.start() resolveSignal = resolveSignal diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index ed4d9de045..18f4345f09 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -137,6 +137,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { private(set) var loadingPlaceholderNode: ChatLoadingPlaceholderNode? var alwaysShowSearchResultsAsList: Bool = false + var includeSavedPeersInSearchResults: Bool = false + private var skippedShowSearchResultsAsListAnimationOnce: Bool = false var inlineSearchResults: ComponentView? private var inlineSearchResultsReadyDisposable: Disposable? @@ -989,7 +991,13 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let emptyNode = ChatEmptyNode(context: self.context, interaction: self.interfaceInteraction) emptyNode.isHidden = self.restrictedNode != nil self.emptyNode = emptyNode - self.historyNodeContainer.supernode?.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) + + if let inlineSearchResultsView = self.inlineSearchResults?.view { + self.contentContainerNode.view.insertSubview(emptyNode.view, belowSubview: inlineSearchResultsView) + } else { + self.contentContainerNode.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) + } + if let (size, insets) = self.validEmptyNodeLayout { let mappedType: ChatEmptyNode.Subject.EmptyType switch emptyType { @@ -2498,7 +2506,9 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } - if let peerId = self.chatPresentationInterfaceState.chatLocation.peerId, displayInlineSearch { + if displayInlineSearch { + let peerId = self.chatPresentationInterfaceState.chatLocation.peerId + let inlineSearchResults: ComponentView var inlineSearchResultsTransition = Transition(transition) if let current = self.inlineSearchResults { @@ -2511,12 +2521,12 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let mappedContents: ChatInlineSearchResultsListComponent.Contents if let _ = self.chatPresentationInterfaceState.search?.resultsState { - mappedContents = .search(query: self.chatPresentationInterfaceState.search?.query ?? "", includeSavedPeers: self.alwaysShowSearchResultsAsList) + mappedContents = .search(query: self.chatPresentationInterfaceState.search?.query ?? "", includeSavedPeers: self.alwaysShowSearchResultsAsList && self.includeSavedPeersInSearchResults) } else if let historyFilter = self.chatPresentationInterfaceState.historyFilter { mappedContents = .tag(historyFilter.customTag) } else if let search = self.chatPresentationInterfaceState.search, self.alwaysShowSearchResultsAsList { if !search.query.isEmpty { - mappedContents = .search(query: search.query, includeSavedPeers: self.alwaysShowSearchResultsAsList) + mappedContents = .search(query: search.query, includeSavedPeers: self.alwaysShowSearchResultsAsList && self.includeSavedPeersInSearchResults) } else { mappedContents = .empty } @@ -2529,6 +2539,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let context = self.context let chatLocation = self.chatLocation + var showEmptyResults = false + if case let .customChatContents(contents) = self.chatPresentationInterfaceState.subject, case .hashTagSearch = contents.kind { + showEmptyResults = true + } + let _ = inlineSearchResults.update( transition: inlineSearchResultsTransition, component: AnyComponent(ChatInlineSearchResultsListComponent( @@ -2544,6 +2559,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { peerId: peerId, contents: mappedContents, insets: childContentInsets, + inputHeight: layout.inputHeight ?? 0.0, + showEmptyResults: showEmptyResults, messageSelected: { [weak self] message in guard let self else { return @@ -2731,46 +2748,51 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { guard let self, let controller = self.controller else { return } - guard let currentSearchState = controller.searchState, let currentResultsState = controller.presentationInterfaceState.search?.resultsState else { - return - } - self.loadMoreSearchResultsDisposable?.dispose() - self.loadMoreSearchResultsDisposable = (self.context.engine.messages.searchMessages(location: currentSearchState.location, query: currentSearchState.query, state: currentResultsState.state) - |> deliverOnMainQueue).startStrict(next: { [weak self] results, updatedState in - guard let self, let controller = self.controller else { + if case let .customChatContents(contents) = self.chatPresentationInterfaceState.subject { + contents.loadMore() + } else { + guard let currentSearchState = controller.searchState, let currentResultsState = controller.presentationInterfaceState.search?.resultsState else { return } - controller.searchResult.set(.single((results, updatedState, currentSearchState.location))) - - var navigateIndex: MessageIndex? - controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in - if let data = current.search { - let messageIndices = results.messages.map({ $0.index }).sorted() - var currentIndex = messageIndices.last - if let previousResultId = data.resultsState?.currentId { - for index in messageIndices { - if index.id >= previousResultId { - currentIndex = index - break + self.loadMoreSearchResultsDisposable?.dispose() + self.loadMoreSearchResultsDisposable = (self.context.engine.messages.searchMessages(location: currentSearchState.location, query: currentSearchState.query, state: currentResultsState.state) + |> deliverOnMainQueue).startStrict(next: { [weak self] results, updatedState in + guard let self, let controller = self.controller else { + return + } + + controller.searchResult.set(.single((results, updatedState, currentSearchState.location))) + + var navigateIndex: MessageIndex? + controller.updateChatPresentationInterfaceState(animated: true, interactive: true, { current in + if let data = current.search { + let messageIndices = results.messages.map({ $0.index }).sorted() + var currentIndex = messageIndices.last + if let previousResultId = data.resultsState?.currentId { + for index in messageIndices { + if index.id >= previousResultId { + currentIndex = index + break + } } } + navigateIndex = currentIndex + return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: updatedState, totalCount: results.totalCount, completed: results.completed))) + } else { + return current + } + }) + if let navigateIndex = navigateIndex { + switch controller.chatLocation { + case .peer, .replyThread, .customChatContents: + controller.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) } - navigateIndex = currentIndex - return current.updatedSearch(data.withUpdatedResultsState(ChatSearchResultsState(messageIndices: messageIndices, currentId: currentIndex?.id, state: updatedState, totalCount: results.totalCount, completed: results.completed))) - } else { - return current } + controller.updateItemNodesSearchTextHighlightStates() }) - if let navigateIndex = navigateIndex { - switch controller.chatLocation { - case .peer, .replyThread, .customChatContents: - controller.navigateToMessage(from: nil, to: .index(navigateIndex), forceInCurrentChat: true) - } - } - controller.updateItemNodesSearchTextHighlightStates() - }) + } } )), environment: {}, @@ -3108,6 +3130,10 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { self.controller?.customNavigationBarContentNode = self.searchNavigationNode } self.searchNavigationNode?.update(presentationInterfaceState: self.chatPresentationInterfaceState) + + if case let .customChatContents(contents) = self.chatPresentationInterfaceState.subject, case .hashTagSearch = contents.kind { + activate = false + } if activate { self.searchNavigationNode?.activate() } @@ -3666,7 +3692,11 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { } } } else { - self.historyNode.scrollScreenToTop() + if let inlineSearchResultsView = self.inlineSearchResults?.view as? ChatInlineSearchResultsListComponent.View { + inlineSearchResultsView.scrollToTop() + } else { + self.historyNode.scrollScreenToTop() + } } } @@ -4024,6 +4054,8 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var postEmptyMessages = false if case let .customChatContents(customChatContents) = self.chatPresentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 171c9805d2..292b53b3d9 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -57,6 +57,8 @@ func serviceTasksForChatPresentationIntefaceState(context: AccountContext, chatP func inputContextQueriesForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) -> [ChatPresentationInputQuery] { if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + return [] case .quickReplyMessageInput: break case .businessLinkSetup: @@ -231,6 +233,8 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index f96d735bbf..a317165c29 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -1988,6 +1988,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState actions.removeAll() switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: if !messageText.isEmpty || (resourceAvailable && isImage) || diceEmoji != nil { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, icon: { theme in diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift index 98cef19649..467dedcb39 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateInputPanels.swift @@ -52,7 +52,12 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState currentPanel.interfaceInteraction = interfaceInteraction return (currentPanel, selectionPanel) } else { - let panel = ChatTagSearchInputPanelNode(theme: chatPresentationInterfaceState.theme) + var alwaysShowTotalMessagesCount = false + if case let .customChatContents(contents) = chatPresentationInterfaceState.subject, case .hashTagSearch = contents.kind { + alwaysShowTotalMessagesCount = true + } + + let panel = ChatTagSearchInputPanelNode(theme: chatPresentationInterfaceState.theme, alwaysShowTotalMessagesCount: alwaysShowTotalMessagesCount) panel.context = context panel.interfaceInteraction = interfaceInteraction return (panel, selectionPanel) @@ -403,6 +408,8 @@ func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState if case let .customChatContents(customChatContents) = chatPresentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + displayInputTextPanel = false case .quickReplyMessageInput, .businessLinkSetup: displayInputTextPanel = true } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift index b96441ca5f..32d85e591d 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateNavigationButtons.swift @@ -57,6 +57,8 @@ func leftNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Cha if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput, .businessLinkSetup: if let currentButton = currentButton, currentButton.action == .dismiss { return currentButton @@ -124,6 +126,8 @@ func rightNavigationButtonForChatInterfaceState(context: AccountContext, present if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + return nil case let .quickReplyMessageInput(_, shortcutType): switch shortcutType { case .generic: diff --git a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift index 2f8c67a0d9..67d0684389 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceTitlePanelNodes.swift @@ -54,6 +54,8 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat inhibitTitlePanelDisplay = true case let .customChatContents(customChatContents): switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: diff --git a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift index 5ba7e6c67b..0e529f49c8 100644 --- a/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatRestrictedInputPanelNode.swift @@ -103,6 +103,8 @@ final class ChatRestrictedInputPanelNode: ChatInputPanelNode { } else if case let .customChatContents(customChatContents) = interfaceState.subject { let displayCount: Int switch customChatContents.kind { + case .hashTagSearch: + displayCount = 0 case .quickReplyMessageInput: displayCount = customChatContents.messageLimit ?? 20 case .businessLinkSetup: diff --git a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift index 08755943c2..ae0a6a3767 100644 --- a/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTagSearchInputPanelNode.swift @@ -91,6 +91,8 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { private var isUpdating: Bool = false + private var alwaysShowTotalMessagesCount = false + private var currentLayout: Layout? private var tagMessageCount: (tag: MemoryBuffer, count: Int?, disposable: Disposable?)? @@ -103,7 +105,9 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } } - init(theme: PresentationTheme) { + init(theme: PresentationTheme, alwaysShowTotalMessagesCount: Bool) { + self.alwaysShowTotalMessagesCount = alwaysShowTotalMessagesCount + super.init() } @@ -224,7 +228,7 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { if let currentId = results.currentId, let index = results.messageIndices.firstIndex(where: { $0.id == currentId }) { canChangeListMode = true - if params.interfaceState.displayHistoryFilterAsList { + if params.interfaceState.displayHistoryFilterAsList || self.alwaysShowTotalMessagesCount { resultsTextString = extractAnimatedTextString(string: params.interfaceState.strings.Chat_BottomSearchPanel_MessageCountFormat( ".", "." @@ -332,36 +336,41 @@ final class ChatTagSearchInputPanelNode: ChatInputPanelNode { } } - var nextLeftX: CGFloat = 12.0 + var nextLeftX: CGFloat = 16.0 - let calendarButtonSize = self.calendarButton.update( - transition: .immediate, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Search/Calendar", - tintColor: params.interfaceState.theme.rootController.navigationBar.accentTextColor - )), - effectAlignment: .center, - minSize: CGSize(width: 1.0, height: size.height), - contentInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), - action: { [weak self] in - guard let self else { - return + if !self.alwaysShowTotalMessagesCount { + nextLeftX = 12.0 + let calendarButtonSize = self.calendarButton.update( + transition: .immediate, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Search/Calendar", + tintColor: params.interfaceState.theme.rootController.navigationBar.accentTextColor + )), + effectAlignment: .center, + minSize: CGSize(width: 1.0, height: size.height), + contentInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), + action: { [weak self] in + guard let self else { + return + } + self.interfaceInteraction?.openCalendarSearch() } - self.interfaceInteraction?.openCalendarSearch() + )), + environment: {}, + containerSize: size + ) + let calendarButtonFrame = CGRect(origin: CGPoint(x: nextLeftX, y: floor((size.height - calendarButtonSize.height) * 0.5)), size: calendarButtonSize) + if let calendarButtonView = self.calendarButton.view { + if calendarButtonView.superview == nil { + self.view.addSubview(calendarButtonView) } - )), - environment: {}, - containerSize: size - ) - let calendarButtonFrame = CGRect(origin: CGPoint(x: nextLeftX, y: floor((size.height - calendarButtonSize.height) * 0.5)), size: calendarButtonSize) - if let calendarButtonView = self.calendarButton.view { - if calendarButtonView.superview == nil { - self.view.addSubview(calendarButtonView) + transition.setFrame(view: calendarButtonView, frame: calendarButtonFrame) } - transition.setFrame(view: calendarButtonView, frame: calendarButtonFrame) + nextLeftX += calendarButtonSize.width + 8.0 + } else if let calendarButtonView = self.calendarButton.view { + calendarButtonView.removeFromSuperview() } - nextLeftX += calendarButtonSize.width + 8.0 if displaySearchMembers { let membersButton: ComponentView diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 61084aecd2..2b89509a57 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -430,6 +430,7 @@ enum ChatTextInputPanelPasteData { case video(Data) case gif(Data) case sticker(UIImage, Bool) + case animatedSticker(Data) } final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { @@ -1497,6 +1498,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var displayMediaButton = true if case let .customChatContents(customChatContents) = interfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: @@ -1863,6 +1866,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } if case let .customChatContents(customChatContents) = interfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + placeholder = "" case let .quickReplyMessageInput(_, shortcutType): switch shortcutType { case .generic: @@ -1895,6 +1900,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let interfaceState = self.presentationInterfaceState { if case let .customChatContents(customChatContents) = interfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: @@ -3673,6 +3680,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if case let .customChatContents(customChatContents) = presentationInterfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: @@ -3791,6 +3800,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch if let interfaceState = self.presentationInterfaceState { if case let .customChatContents(customChatContents) = interfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: @@ -3894,6 +3905,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch var sendButtonHasApplyIcon = interfaceState.interfaceState.editMessage != nil if case let .customChatContents(customChatContents) = interfaceState.subject { switch customChatContents.kind { + case .hashTagSearch: + break case .quickReplyMessageInput: break case .businessLinkSetup: @@ -4379,6 +4392,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch } else if let data = pasteboard.data(forPasteboardType: "public.mpeg-4") { self.paste(.video(data)) return false + } else if let data = pasteboard.data(forPasteboardType: "public.heics") { + self.paste(.animatedSticker(data)) + return false } else { var isPNG = false var isMemoji = false diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index c734108683..cba2c06b8c 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -103,6 +103,7 @@ private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { case localThemes = 3 case storyDrafts = 4 case storySources = 5 + case hashtagSearchRecentQueries = 6 } public struct ApplicationSpecificOrderedItemListCollectionId { @@ -112,4 +113,5 @@ public struct ApplicationSpecificOrderedItemListCollectionId { public static let localThemes = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.localThemes.rawValue) public static let storyDrafts = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storyDrafts.rawValue) public static let storySources = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.storySources.rawValue) + public static let hashtagSearchRecentQueries = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.hashtagSearchRecentQueries.rawValue) } From f77b7a7b330bbc58027a72a974ac4f8d2d6ccb95 Mon Sep 17 00:00:00 2001 From: Mikhail Filimonov Date: Wed, 22 May 2024 17:20:29 +0400 Subject: [PATCH 5/8] fact chect attribute --- .../TelegramCore/Sources/Utils/MessageUtils.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift index f53cbaa8bf..741799e6c3 100644 --- a/submodules/TelegramCore/Sources/Utils/MessageUtils.swift +++ b/submodules/TelegramCore/Sources/Utils/MessageUtils.swift @@ -441,6 +441,16 @@ public extension Message { } return nil } + + var factCheckAttribute: FactCheckMessageAttribute? { + for attribute in self.attributes { + if let attribute = attribute as? FactCheckMessageAttribute { + return attribute + } + } + return nil + } + var inlineBotAttribute: InlineBusinessBotMessageAttribute? { for attribute in self.attributes { if let attribute = attribute as? InlineBusinessBotMessageAttribute { From 418968604c27a868584201f2647bb9fe16fcbeb8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 22 May 2024 18:23:32 +0400 Subject: [PATCH 6/8] Various fixes --- .../Sources/ChatController.swift | 1 + .../Sources/HashtagSearchControllerNode.swift | 23 +++++++++++---- .../HashtagSearchNavigationContentNode.swift | 17 +++++++---- .../Sources/HashtagSearchRecentListNode.swift | 18 ++++++++---- .../Messages/SearchMessages.swift | 4 ++- .../Sources/StarsTransactionScreen.swift | 6 +++- .../ChatControllerNavigateToMessage.swift | 12 ++++++-- .../TelegramUI/Sources/ChatController.swift | 28 ++++++++++++------- .../Sources/ChatControllerNode.swift | 20 ++++++++----- 9 files changed, 92 insertions(+), 37 deletions(-) diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index bf4e60edfc..223e29bb5d 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -990,6 +990,7 @@ public protocol ChatController: ViewController { var alwaysShowSearchResultsAsList: Bool { get set } var includeSavedPeersInSearchResults: Bool { get set } + var showListEmptyResults: Bool { get set } func updatePresentationMode(_ mode: ChatControllerPresentationMode) func beginMessageSearch(_ query: String) diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index e1fc79ae27..d18914e627 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -45,7 +45,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let cleanHashtag = query.replacingOccurrences(of: "#", with: "") - self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: cleanHashtag, cancel: { [weak controller] in + self.searchContentNode = HashtagSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, initialQuery: cleanHashtag, hasCurrentChat: peer != nil, cancel: { [weak controller] in controller?.dismiss() }) @@ -59,6 +59,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { if let peer { self.currentController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peer.id), subject: nil, botStart: nil, mode: .inline(navigationController)) self.currentController?.alwaysShowSearchResultsAsList = true + self.currentController?.showListEmptyResults = true self.currentController?.customNavigationController = navigationController } else { self.currentController = nil @@ -70,12 +71,17 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.myChatContents = myChatContents self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default)) self.myController?.alwaysShowSearchResultsAsList = true + self.myController?.showListEmptyResults = true self.myController?.customNavigationController = navigationController + if peer == nil { + self.searchContentNode.selectedIndex = 1 + } let globalChatContents = HashtagSearchGlobalChatContents(context: context, kind: .hashTagSearch, query: cleanHashtag, onlyMy: false) self.globalChatContents = globalChatContents self.globalController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: globalChatContents), botStart: nil, mode: .standard(.default)) self.globalController?.alwaysShowSearchResultsAsList = true + self.globalController?.showListEmptyResults = true self.globalController?.customNavigationController = navigationController super.init() @@ -88,10 +94,17 @@ final class HashtagSearchControllerNode: ASDisplayNode { if controller.all { self.currentController?.displayNode.isHidden = true - } else { - self.currentController?.displayNode.isHidden = false - self.myController?.displayNode.isHidden = true + self.myController?.displayNode.isHidden = false self.globalController?.displayNode.isHidden = true + } else { + if let _ = peer { + self.currentController?.displayNode.isHidden = false + self.myController?.displayNode.isHidden = true + self.globalController?.displayNode.isHidden = true + } else { + self.myController?.displayNode.isHidden = false + self.globalController?.displayNode.isHidden = true + } } self.searchContentNode.indexUpdated = { [weak self] index in @@ -140,7 +153,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { self?.searchQueryPromise.set(query) } - let _ = addRecentHashtagSearchQuery(engine: context.engine, string: query).startStandalone() + let _ = addRecentHashtagSearchQuery(engine: context.engine, string: query.replacingOccurrences(of: "#", with: "")).startStandalone() self.searchContentNode.onReturn = { query in let _ = addRecentHashtagSearchQuery(engine: context.engine, string: query).startStandalone() } diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift index 151e77629d..83c195efe6 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -15,6 +15,7 @@ private let searchBarFont = Font.regular(17.0) final class HashtagSearchNavigationContentNode: NavigationBarContentNode { private var theme: PresentationTheme private let strings: PresentationStrings + private let hasCurrentChat: Bool private let cancel: () -> Void @@ -49,9 +50,10 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { } } - init(theme: PresentationTheme, strings: PresentationStrings, initialQuery: String, cancel: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, initialQuery: String, hasCurrentChat: Bool, cancel: @escaping () -> Void) { self.theme = theme self.strings = strings + self.hasCurrentChat = hasCurrentChat self.cancel = cancel @@ -101,6 +103,13 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { self.searchBar.frame = searchBarFrame self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: leftInset + sideInset, rightInset: rightInset + sideInset, transition: transition) + var items: [TabSelectorComponent.Item] = [] + if self.hasCurrentChat { + items.append(TabSelectorComponent.Item(id: AnyHashable(0), title: self.strings.HashtagSearch_ThisChat)) + } + items.append(TabSelectorComponent.Item(id: AnyHashable(1), title: self.strings.HashtagSearch_MyMessages)) + items.append(TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts)) + let tabSelectorSize = self.tabSelector.update( transition: Transition(transition), component: AnyComponent(TabSelectorComponent( @@ -113,11 +122,7 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { spacing: 24.0, lineSelection: true ), - items: [ - TabSelectorComponent.Item(id: AnyHashable(0), title: self.strings.HashtagSearch_ThisChat), - TabSelectorComponent.Item(id: AnyHashable(1), title: self.strings.HashtagSearch_MyMessages), - TabSelectorComponent.Item(id: AnyHashable(2), title: self.strings.HashtagSearch_PublicPosts) - ], + items: items, selectedId: AnyHashable(self.selectedIndex), setSelectedId: { [weak self] id in guard let self, let index = id.base as? Int else { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift index 4ce0ebb0cd..b623808257 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchRecentListNode.swift @@ -287,13 +287,13 @@ final class HashtagSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { strongSelf.addSubnode(textNode) } - let textFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) + let textFrame = CGRect(origin: CGPoint(x: leftInset + strongSelf.revealOffset, y: floorToScreenPixels((nodeLayout.contentSize.height - textLayout.size.height) / 2.0)), size: textLayout.size) textNode.frame = textFrame if let icon = strongSelf.iconNode.image { - strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - icon.size.width - 16.0, y: floorToScreenPixels((nodeLayout.contentSize.height - icon.size.height) / 2.0)), size: icon.size) + strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - icon.size.width - 16.0 + strongSelf.revealOffset, y: floorToScreenPixels((nodeLayout.contentSize.height - icon.size.height) / 2.0)), size: icon.size) } - + let separatorHeight = UIScreenPixel let topHighlightInset: CGFloat = separatorHeight @@ -304,7 +304,11 @@ final class HashtagSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { strongSelf.updateLayout(size: nodeLayout.contentSize, leftInset: params.leftInset, rightInset: params.rightInset) - strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + if item.clear { + strongSelf.setRevealOptions((left: [], right: [])) + } else { + strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: RevealOptionKey.delete.rawValue, title: item.strings.Common_Delete, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)])) + } } }) }) @@ -331,11 +335,15 @@ final class HashtagSearchRecentQueryItemNode: ItemListRevealOptionsItemNode { super.updateRevealOffset(offset: offset, transition: transition) if let params = self.layoutParams, let textNode = self.textNode { - let leftInset: CGFloat = 15.0 + params.leftInset + let leftInset: CGFloat = 62.0 + params.leftInset var textFrame = textNode.frame textFrame.origin.x = leftInset + offset transition.updateFrame(node: textNode, frame: textFrame) + + var iconFrame = self.iconNode.frame + iconFrame.origin.x = textFrame.minX - iconFrame.width - 16.0 + transition.updateFrame(node: self.iconNode, frame: iconFrame) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift index e8e4760a1d..84aedac1f2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SearchMessages.swift @@ -516,10 +516,12 @@ func _internal_searchMessages(account: Account, location: SearchMessagesLocation func _internal_searchHashtagPosts(account: Account, hashtag: String, state: SearchMessagesState?, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> { let remoteSearchResult = account.postbox.transaction { transaction -> (Int32, MessageIndex?, Api.InputPeer) in var lowerBound: MessageIndex? + var peer: Peer? if let state = state, let message = state.main.messages.last { lowerBound = message.index + peer = message.peers[message.id.peerId] } - if let lowerBound = lowerBound, let peer = transaction.getPeer(lowerBound.id.peerId), let inputPeer = apiInputPeer(peer) { + if let lowerBound = lowerBound, let peer, let inputPeer = apiInputPeer(peer) { return (state?.main.nextRate ?? 0, lowerBound, inputPeer) } else { return (0, lowerBound, .inputPeerEmpty) diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift index 19f371a0bb..584423ebb0 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift @@ -259,7 +259,11 @@ private final class StarsTransactionSheetContent: CombinedComponent { id: "transaction", title: "Transaction ID", component: AnyComponent( - MultilineTextComponent(text: .plain(NSAttributedString(string: transactionId, font: tableFont, textColor: tableTextColor)), truncationType: .middle) + MultilineTextComponent( + text: .plain(NSAttributedString(string: transactionId, font: Font.monospace(15.0), textColor: tableTextColor)), + truncationType: .end, + maximumNumberOfLines: 0 + ) ) )) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift index d53572aab0..41fe42586f 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerNavigateToMessage.swift @@ -85,8 +85,10 @@ extension ChatControllerImpl { } var fromIndex: MessageIndex? + var fromMessage: Message? if let fromId = fromId, let message = self.chatDisplayNode.historyNode.messageInCurrentHistoryView(fromId) { fromIndex = message.index + fromMessage = message } else { if let message = self.chatDisplayNode.historyNode.anchorMessageInCurrentHistoryView() { fromIndex = message.index @@ -110,9 +112,15 @@ extension ChatControllerImpl { } if isPinnedMessages || forceNew, let messageId = messageLocation.messageId { + let peerSignal: Signal + if forceNew, let fromMessage, let peer = fromMessage.peers[fromMessage.id.peerId] { + peerSignal = .single(EnginePeer(peer)) + } else { + peerSignal = self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)) + } let _ = (combineLatest( - self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId)), - self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: .local) + peerSignal, + self.context.engine.messages.getMessagesLoadIfNecessary([messageId], strategy: forceNew ? .cloud(skipLocal: false) : .local) |> `catch` { _ in return .single(.result([])) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6dcb92f230..75f5ae1f65 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -614,6 +614,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } + public var showListEmptyResults: Bool = false { + didSet { + self.chatDisplayNode.showListEmptyResults = self.showListEmptyResults + } + } + public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(.default), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = [], customChatNavigationStack: [EnginePeer.Id]? = nil) { let _ = ChatControllerCount.modify { value in return value + 1 @@ -9525,9 +9531,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func openHashtag(_ hashtag: String, peerName: String?) { - guard let peerId = self.chatLocation.peerId else { - return - } let _ = self.presentVoiceMessageDiscardAlert(action: { if self.resolvePeerByNameDisposable == nil { self.resolvePeerByNameDisposable = MetaDisposable() @@ -9548,9 +9551,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return .single(nil) } } - } else { + } else if let peerId = self.chatLocation.peerId { resolveSignal = self.context.account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) + } else { + resolveSignal = .single(nil) } var cancelImpl: (() -> Void)? let presentationData = self.presentationData @@ -9695,11 +9700,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func openResolved(result: ResolvedUrl, sourceMessageId: MessageId?, progress: Promise? = nil, forceExternal: Bool = false, concealed: Bool = false, commit: @escaping () -> Void = {}) { - guard let peerId = self.chatLocation.peerId else { - return - } + let urlContext: OpenURLContext + let message = sourceMessageId.flatMap { self.chatDisplayNode.historyNode.messageInCurrentHistoryView($0) } - self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: .chat(peerId: peerId, message: message, updatedPresentationData: self.updatedPresentationData), navigationController: self.effectiveNavigationController, forceExternal: forceExternal, openPeer: { [weak self] peerId, navigation in + if let peerId = self.chatLocation.peerId { + urlContext = .chat(peerId: peerId, message: message, updatedPresentationData: self.updatedPresentationData) + } else { + urlContext = .generic + } + self.context.sharedContext.openResolvedUrl(result, context: self.context, urlContext: urlContext, navigationController: self.effectiveNavigationController, forceExternal: forceExternal, openPeer: { [weak self] peerId, navigation in guard let strongSelf = self else { return } @@ -9765,8 +9774,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: break } - }, sendFile: nil, - sendSticker: { [weak self] f, sourceView, sourceRect in + }, sendFile: nil, sendSticker: { [weak self] f, sourceView, sourceRect in return self?.interfaceInteraction?.sendSticker(f, true, sourceView, sourceRect, nil, []) ?? false }, sendEmoji: { [weak self] text, attribute in guard let self, canSendMessagesToChat(self.presentationInterfaceState) else { diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 18f4345f09..079456553b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -138,6 +138,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { var alwaysShowSearchResultsAsList: Bool = false var includeSavedPeersInSearchResults: Bool = false + var showListEmptyResults: Bool = false private var skippedShowSearchResultsAsListAnimationOnce: Bool = false var inlineSearchResults: ComponentView? @@ -2539,11 +2540,6 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { let context = self.context let chatLocation = self.chatLocation - var showEmptyResults = false - if case let .customChatContents(contents) = self.chatPresentationInterfaceState.subject, case .hashTagSearch = contents.kind { - showEmptyResults = true - } - let _ = inlineSearchResults.update( transition: inlineSearchResultsTransition, component: AnyComponent(ChatInlineSearchResultsListComponent( @@ -2560,13 +2556,23 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate { contents: mappedContents, insets: childContentInsets, inputHeight: layout.inputHeight ?? 0.0, - showEmptyResults: showEmptyResults, + showEmptyResults: self.showListEmptyResults, messageSelected: { [weak self] message in guard let self else { return } - if let historyFilter = self.chatPresentationInterfaceState.historyFilter, let reaction = ReactionsMessageAttribute.reactionFromMessageTag(tag: historyFilter.customTag), let peerId = self.chatLocation.peerId, historyFilter.isActive { + if case let .customChatContents(contents) = self.chatPresentationInterfaceState.subject, case .hashTagSearch = contents.kind { + self.controller?.navigateToMessage( + from: message.id, + to: .index(message.index), + scrollPosition: .center(.bottom), + rememberInStack: false, + forceInCurrentChat: false, + forceNew: true, + animated: true + ) + } else if let historyFilter = self.chatPresentationInterfaceState.historyFilter, let reaction = ReactionsMessageAttribute.reactionFromMessageTag(tag: historyFilter.customTag), let peerId = self.chatLocation.peerId, historyFilter.isActive { let _ = (self.context.engine.messages.searchMessages( location: .peer(peerId: peerId, fromId: nil, tags: nil, reactions: [reaction], threadId: self.chatLocation.threadId, minDate: nil, maxDate: nil), query: "", From bd9c495fb4bbb5d974d872f2cdfc41b1364cade0 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 22 May 2024 19:12:37 +0400 Subject: [PATCH 7/8] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 26 +++++++++++++++++ .../Sources/HashtagSearchControllerNode.swift | 10 +++++-- .../Sources/StarsTransactionScreen.swift | 14 +++++----- .../Sources/StarsTransactionsScreen.swift | 15 +++++----- .../Sources/StarsTransferScreen.swift | 28 +++++++++++++------ 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index d8dd72eb9c..50df79bff4 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12226,6 +12226,13 @@ Sorry for the inconvenience."; "Conversation.ContextMenuAddFactCheck" = "Add Fact Check"; "Conversation.ContextMenuEditFactCheck" = "Edit Fact Check"; +"Stars.Intro.Title" = "Telegram Stars"; +"Stars.Intro.Description" = "Buy Stars to unlock content and services in miniapps on Telegram."; +"Stars.Intro.Balance" = "Balance"; +"Stars.Intro.AllTransactions" = "All Transactions"; +"Stars.Intro.Incoming" = "Incoming"; +"Stars.Intro.Outgoing" = "Outgoing"; + "Stars.Purchase.GetStars" = "Get Stars"; "Stars.Purchase.GetStarsInfo" = "Choose how many Stars you would like to buy."; @@ -12241,4 +12248,23 @@ Sorry for the inconvenience."; "Stars.Purchase.Info" = "By proceeding and purchasing Stars, you agree with [Terms and Conditions]()."; "Stars.Purchase.Terms_URL" = "https://telegram.org/tos"; +"Stars.Transaction.To" = "To"; +"Stars.Transaction.Id" = "Transaction ID"; +"Stars.Transaction.Date" = "Date"; +"Stars.Transaction.Terms" = "Review the [Terms of Service]() for Stars."; +"Stars.Transaction.Terms_URL" = "https://telegram.org/tos"; +"Stars.Transaction.CopiedId" = "Transaction ID copied to clipboard."; + +"Stars.Transfer.Title" = "Confirm Your Purchase"; +"Stars.Transfer.Info" = "Do you want to buy **%1$@** in **%2$@** for **%3$@**?"; +"Stars.Transfer.Info.Stars_1" = "%@ Star"; +"Stars.Transfer.Info.Stars_any" = "%@ Stars"; +"Stars.Transfer.Pay" = "Confirm and Pay"; +"Stars.Transfer.PurchasedTitle" = "Purchase Completed"; +"Stars.Transfer.PurchasedText" = "You acquired **%1$@** in **%2$@** for **%3$@**."; +"Stars.Transfer.Purchased.Stars_1" = "%@ Star"; +"Stars.Transfer.Purchased.Stars_any" = "%@ Stars"; + +"Stars.Transfer.Balance" = "Balance"; + "Settings.Stars" = "Your Stars"; diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift index d18914e627..df4143f00b 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchControllerNode.swift @@ -64,9 +64,7 @@ final class HashtagSearchControllerNode: ASDisplayNode { } else { self.currentController = nil } - - self.isSearching.set(self.currentController?.searching.get() ?? .single(false)) - + let myChatContents = HashtagSearchGlobalChatContents(context: context, kind: .hashTagSearch, query: cleanHashtag, onlyMy: true) self.myChatContents = myChatContents self.myController = context.sharedContext.makeChatController(context: context, chatLocation: .customChatContents, subject: .customChatContents(contents: myChatContents), botStart: nil, mode: .standard(.default)) @@ -96,14 +94,20 @@ final class HashtagSearchControllerNode: ASDisplayNode { self.currentController?.displayNode.isHidden = true self.myController?.displayNode.isHidden = false self.globalController?.displayNode.isHidden = true + + self.isSearching.set(self.myChatContents?.searching ?? .single(false)) } else { if let _ = peer { self.currentController?.displayNode.isHidden = false self.myController?.displayNode.isHidden = true self.globalController?.displayNode.isHidden = true + + self.isSearching.set(self.currentController?.searching.get() ?? .single(false)) } else { self.myController?.displayNode.isHidden = false self.globalController?.displayNode.isHidden = true + + self.isSearching.set(self.myChatContents?.searching ?? .single(false)) } } diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift index 584423ebb0..81da4e62d2 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift @@ -169,8 +169,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { } else { descriptionText = "+ \(transaction.count) ⭐️" } - additionalText = "You can dispute this transaction [here]()." - buttonText = "OK" + additionalText = strings.Stars_Transaction_Terms + buttonText = strings.Common_OK transactionId = transaction.id date = transaction.date @@ -207,7 +207,6 @@ private final class StarsTransactionSheetContent: CombinedComponent { avatarSize: 90.0, color: UIColor(rgb: 0xf7ab04) ), -// PremiumStarComponent(isIntro: false, isVisible: true, hasIdleAnimations: true), availableSize: CGSize(width: context.availableSize.width, height: 200.0), transition: .immediate ) @@ -238,7 +237,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { if let toPeer { tableItems.append(.init( id: "to", - title: strings.GiftLink_To, + title: strings.Stars_Transaction_Date, component: AnyComponent( Button( content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), @@ -257,7 +256,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { tableItems.append(.init( id: "transaction", - title: "Transaction ID", + title: strings.Stars_Transaction_Id, component: AnyComponent( MultilineTextComponent( text: .plain(NSAttributedString(string: transactionId, font: Font.monospace(15.0), textColor: tableTextColor)), @@ -269,7 +268,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { tableItems.append(.init( id: "date", - title: strings.GiftLink_Date, + title: strings.Stars_Transaction_Date, component: AnyComponent( MultilineTextComponent(text: .plain(NSAttributedString(string: stringForMediumDate(timestamp: date, strings: strings, dateTimeFormat: dateTimeFormat), font: tableFont, textColor: tableTextColor))) ) @@ -299,7 +298,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { } }, tapAction: { attributes, _ in - + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + component.context.sharedContext.openExternalUrl(context: component.context, urlContext: .generic, url: strings.Stars_Transaction_Terms_URL, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {}) } ), availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift index 4522f76b66..248b1b8876 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsScreen.swift @@ -392,7 +392,7 @@ final class StarsTransactionsScreenComponent: Component { transition: .immediate, component: AnyComponent( MultilineTextComponent( - text: .plain(NSAttributedString(string: "Telegram Stars", font: Font.bold(28.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_Title, font: Font.bold(28.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -415,8 +415,8 @@ final class StarsTransactionsScreenComponent: Component { let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) - let balanceAttributedString = parseMarkdownIntoAttributedString(" Balance\n > **\(starsState?.balance ?? 0)**", attributes: markdownAttributes, textAlignment: .right).mutableCopy() as! NSMutableAttributedString - if let range = balanceAttributedString.string.range(of: ">"), let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903)) { + let balanceAttributedString = parseMarkdownIntoAttributedString(" \(environment.strings.Stars_Intro_Balance)\n # **\(self.starsState?.balance ?? 0)**", attributes: markdownAttributes, textAlignment: .right).mutableCopy() as! NSMutableAttributedString + if let range = balanceAttributedString.string.range(of: "#"), let chevronImage = generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: UIColor(rgb: 0xf09903)) { balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: balanceAttributedString.string)) @@ -446,7 +446,7 @@ final class StarsTransactionsScreenComponent: Component { transition: .immediate, component: AnyComponent( BalancedTextComponent( - text: .plain(NSAttributedString(string: "Buy Stars to unlock content and services in miniapps on Telegram.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.Stars_Intro_Description, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -501,7 +501,6 @@ final class StarsTransactionsScreenComponent: Component { contentHeight += balanceSize.height contentHeight += 44.0 - //TODO: localize let transactions = self.starsState?.transactions ?? [] let allItems = StarsTransactionsListPanelComponent.Items( items: transactions.map { StarsTransactionsListPanelComponent.Item(transaction: $0) } @@ -517,7 +516,7 @@ final class StarsTransactionsScreenComponent: Component { if !allItems.items.isEmpty { panelItems.append(StarsTransactionsPanelContainerComponent.Item( id: "all", - title: "All Transactions", + title: environment.strings.Stars_Intro_AllTransactions, panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, items: allItems, @@ -530,7 +529,7 @@ final class StarsTransactionsScreenComponent: Component { if !outgoingItems.items.isEmpty { panelItems.append(StarsTransactionsPanelContainerComponent.Item( id: "incoming", - title: "Incoming", + title: environment.strings.Stars_Intro_Incoming, panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, items: incomingItems, @@ -542,7 +541,7 @@ final class StarsTransactionsScreenComponent: Component { panelItems.append(StarsTransactionsPanelContainerComponent.Item( id: "outgoing", - title: "Outgoing", + title: environment.strings.Stars_Intro_Outgoing, panel: AnyComponent(StarsTransactionsListPanelComponent( context: component.context, items: outgoingItems, diff --git a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift index f09f5e029c..d5a25d275b 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransferScreen/Sources/StarsTransferScreen.swift @@ -156,7 +156,7 @@ private final class SheetContent: CombinedComponent { let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme -// let strings = presentationData.strings + let strings = presentationData.strings // let sideInset: CGFloat = 16.0 + environment.safeInsets.left @@ -216,10 +216,9 @@ private final class SheetContent: CombinedComponent { let constrainedTitleWidth = context.availableSize.width - 16.0 * 2.0 - contentSize.height += 130.0 let title = title.update( - component: Text(text: "Confirm Your Purchase", font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), + component: Text(text: strings.Stars_Transfer_Title, font: Font.bold(24.0), color: theme.list.itemPrimaryTextColor), availableSize: CGSize(width: constrainedTitleWidth, height: context.availableSize.height), transition: .immediate ) @@ -240,7 +239,14 @@ private final class SheetContent: CombinedComponent { let amount = component.invoice.totalAmount let text = text.update( component: BalancedTextComponent( - text: .markdown(text: "Do you want to buy **\(component.invoice.title)** in **\(state.peer?.compactDisplayTitle ?? "")** for **\(amount) Stars**?", attributes: markdownAttributes), + text: .markdown( + text: strings.Stars_Transfer_Info( + component.invoice.title, + state.peer?.compactDisplayTitle ?? "", + strings.Stars_Transfer_Info_Stars(Int32(amount)) + ).string, + attributes: markdownAttributes + ), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -258,7 +264,7 @@ private final class SheetContent: CombinedComponent { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Star"), color: UIColor(rgb: 0xf09903))!, theme) } - let balanceAttributedString = parseMarkdownIntoAttributedString("Balance\n # **\(state.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString + let balanceAttributedString = parseMarkdownIntoAttributedString("\(strings.Stars_Transfer_Balance)\n # **\(state.balance ?? 0)**", attributes: markdownAttributes).mutableCopy() as! NSMutableAttributedString if let range = balanceAttributedString.string.range(of: "#"), let chevronImage = state.cachedChevronImage?.0 { balanceAttributedString.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: balanceAttributedString.string)) balanceAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xf09903), range: NSRange(range, in: balanceAttributedString.string)) @@ -281,8 +287,8 @@ private final class SheetContent: CombinedComponent { state.cachedStarImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/PremiumIcon"), color: .white)!, theme) } - let buttonAttributedString = NSMutableAttributedString(string: "Confirm and Pay > \(amount)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) - if let range = buttonAttributedString.string.range(of: ">"), let starImage = state.cachedStarImage?.0 { + let buttonAttributedString = NSMutableAttributedString(string: "\(strings.Stars_Transfer_Pay) # \(amount)", font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) + if let range = buttonAttributedString.string.range(of: "#"), let starImage = state.cachedStarImage?.0 { buttonAttributedString.addAttribute(.attachment, value: starImage, range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.foregroundColor, value: UIColor(rgb: 0xffffff), range: NSRange(range, in: buttonAttributedString.string)) buttonAttributedString.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: buttonAttributedString.string)) @@ -323,7 +329,13 @@ private final class SheetContent: CombinedComponent { let presentationData = accountContext.sharedContext.currentPresentationData.with { $0 } let resultController = UndoOverlayController( presentationData: presentationData, - content: .image(image: UIImage(bundleImageName: "Premium/Stars/Star")!, title: "Purchase Completed", text: "You acquired **\(invoice.title)** in **\(botTitle)** for **\(invoice.totalAmount) Stars**.", round: false, undoText: nil), + content: .image( + image: UIImage(bundleImageName: "Premium/Stars/Star")!, + title: presentationData.strings.Stars_Transfer_PurchasedTitle, + text: presentationData.strings.Stars_Transfer_PurchasedText(invoice.title, botTitle, presentationData.strings.Stars_Transfer_Purchased_Stars(Int32(invoice.totalAmount))).string, + round: false, + undoText: nil + ), elevatedLayout: true, action: { _ in return true}) controller?.present(resultController, in: .window(.root)) From 4e55e3aab0902379e2fa292582845bafdc76306a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Thu, 23 May 2024 19:42:13 +0400 Subject: [PATCH 8/8] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../HashtagSearchNavigationContentNode.swift | 24 +- .../Sources/PremiumCoinComponent.swift | 7 +- ...hatMessageFactCheckBubbleContentNode.swift | 188 +++++++----- .../Sources/PeerInfoScreen.swift | 2 +- .../Sources/StarsTransactionScreen.swift | 279 +++++++++++++++--- versions.json | 2 +- 7 files changed, 370 insertions(+), 133 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 50df79bff4..64b5d4beaa 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -12217,6 +12217,7 @@ Sorry for the inconvenience."; "Message.FactCheck" = "Fact Check"; "Message.FactCheck.WhatIsThis" = "what's this?"; +"Conversation.FactCheck.InnerDescription" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country (%@) responsible for combating misinformation."; "Conversation.FactCheck.Description" = "This clarification was provided by a fact checking agency assigned by the department of the government of your country (%@) responsible for combating misinformation."; "FactCheck.Title" = "Fact Check"; diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift index 83c195efe6..6a48b375f0 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchNavigationContentNode.swift @@ -63,7 +63,9 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { super.init() - self.addSubnode(self.searchBar) + if hasCurrentChat { + self.addSubnode(self.searchBar) + } self.searchBar.cancel = { [weak self] in self?.searchBar.deactivate(clear: false) @@ -79,6 +81,10 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { } } + override var mode: NavigationBarContentMode { + return self.hasCurrentChat ? .replacement : .expansion + } + func updateTheme(_ theme: PresentationTheme) { self.theme = theme self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: self.strings) @@ -89,7 +95,11 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { } override var nominalHeight: CGFloat { - return 54.0 + 44.0 + if self.hasCurrentChat { + return 54.0 + 44.0 + } else { + return 45.0 + } } private var validLayout: (CGSize, CGFloat, CGFloat)? @@ -119,7 +129,7 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { ), customLayout: TabSelectorComponent.CustomLayout( font: Font.medium(14.0), - spacing: 24.0, + spacing: self.hasCurrentChat ? 24.0 : 8.0, lineSelection: true ), items: items, @@ -135,7 +145,13 @@ final class HashtagSearchNavigationContentNode: NavigationBarContentNode { environment: {}, containerSize: CGSize(width: size.width, height: 44.0) ) - let tabSelectorFrame = CGRect(origin: CGPoint(x: floor((size.width - tabSelectorSize.width) / 2.0), y: size.height - tabSelectorSize.height - 9.0), size: tabSelectorSize) + let tabSelectorFrameOriginX: CGFloat + if self.hasCurrentChat || "".isEmpty { + tabSelectorFrameOriginX = floorToScreenPixels((size.width - tabSelectorSize.width) / 2.0) + } else { + tabSelectorFrameOriginX = 4.0 + } + let tabSelectorFrame = CGRect(origin: CGPoint(x: tabSelectorFrameOriginX, y: size.height - tabSelectorSize.height - 9.0), size: tabSelectorSize) if let tabSelectorView = self.tabSelector.view { if tabSelectorView.superview == nil { self.view.addSubview(tabSelectorView) diff --git a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift index 851bdd656a..84a156e022 100644 --- a/submodules/PremiumUI/Sources/PremiumCoinComponent.swift +++ b/submodules/PremiumUI/Sources/PremiumCoinComponent.swift @@ -231,9 +231,10 @@ class PremiumCoinComponent: Component { self.sceneView.scene = scene self.sceneView.delegate = self - self.didSetReady = true - self._ready.set(.single(true)) - self.onReady() + let _ = self.sceneView.snapshot() +// self.didSetReady = true +// self._ready.set(.single(true)) +// self.onReady() } private var didSetReady = false diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift index c5b5b9efad..1b9624e41b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageFactCheckBubbleContentNode/Sources/ChatMessageFactCheckBubbleContentNode.swift @@ -42,8 +42,11 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode private var titleBadgeButton: HighlightTrackingButtonNode? private let textClippingNode: ASDisplayNode private let textNode: TextNode + private let additionalTextNode: TextNode private var linkHighlightingNode: LinkHighlightingNode? + private let lineNode: ASDisplayNode + private var maskView: UIImageView? private var maskOverlayView: UIView? @@ -55,13 +58,17 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode private var isExpanded: Bool = false private var appliedIsExpanded: Bool = false + private var countryName: String? + required public init() { self.titleNode = TextNode() self.titleBadgeLabel = TextNode() self.textClippingNode = ASDisplayNode() self.textNode = TextNode() + self.additionalTextNode = TextNode() self.expandIcon = ASImageNode() self.statusNode = ChatMessageDateAndStatusNode() + self.lineNode = ASDisplayNode() super.init() @@ -80,6 +87,14 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode self.textNode.displaysAsynchronously = false self.textClippingNode.addSubnode(self.textNode) + self.additionalTextNode.isUserInteractionEnabled = false + self.additionalTextNode.contentMode = .topLeft + self.additionalTextNode.contentsScale = UIScreenScale + self.additionalTextNode.displaysAsynchronously = false + self.textClippingNode.addSubnode(self.additionalTextNode) + + self.textClippingNode.addSubnode(self.lineNode) + self.titleBadgeLabel.isUserInteractionEnabled = false self.titleBadgeLabel.contentMode = .topLeft self.titleBadgeLabel.contentsScale = UIScreenScale @@ -106,24 +121,10 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode } @objc private func badgePressed() { - guard let item = self.item else { + guard let item = self.item, let countryName = self.countryName else { return } - var countryId: String? - for attribute in item.message.attributes { - if let attribute = attribute as? FactCheckMessageAttribute, case let .Loaded(_, _, countryIdValue) = attribute.content { - countryId = countryIdValue - break - } - } - - guard let countryId else { - return - } - - let locale = localeWithStrings(item.presentationData.strings) - let countryName = displayCountryName(countryId, locale: locale) item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_FactCheck_Description(countryName).string, true, self.titleBadgeButton, nil) } @@ -148,13 +149,9 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode } let textNodeFrame = self.textClippingNode.frame - if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { + if let (_, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) - } - return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed))) + return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: false))) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { @@ -216,10 +213,12 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let titleLayout = TextNode.asyncLayout(self.titleNode) let titleBadgeLayout = TextNode.asyncLayout(self.titleBadgeLabel) let textLayout = TextNode.asyncLayout(self.textNode) + let additionalTextLayout = TextNode.asyncLayout(self.additionalTextNode) let measureTextLayout = TextNode.asyncLayout(nil) let statusLayout = self.statusNode.asyncLayout() let currentIsExpanded = self.isExpanded + let currentCountryName = self.countryName return { item, layoutConstants, _, _, _, _ in let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none) @@ -313,15 +312,32 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let titleBadgeString = NSAttributedString(string: item.presentationData.strings.Message_FactCheck_WhatIsThis, font: badgeFont, textColor: mainColor) let (titleBadgeLayout, titleBadgeApply) = titleBadgeLayout(TextNodeLayoutArguments(attributedString: titleBadgeString, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: textConstrainedSize)) + let countryName: String + if let currentCountryName { + countryName = currentCountryName + } else { + if let attribute = item.message.factCheckAttribute, case let .Loaded(_, _, countryIdValue) = attribute.content { + let locale = localeWithStrings(item.presentationData.strings) + countryName = displayCountryName(countryIdValue, locale: locale) + } else { + countryName = "" + } + } + let finalAttributedText = stringWithAppliedEntities(rawText, entities: rawEntities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil) as! NSMutableAttributedString finalAttributedText.append(NSAttributedString(string: "__", font: textFont, textColor: .clear)) let (textLayout, textApply) = textLayout(TextNodeLayoutArguments(attributedString: finalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor)) + + let additionalAttributedText = NSMutableAttributedString(string: item.presentationData.strings.Conversation_FactCheck_InnerDescription(countryName).string, font: badgeFont, textColor: mainColor) + additionalAttributedText.append(NSAttributedString(string: "__", font: badgeFont, textColor: .clear)) + + let (additionalTextLayout, additionalTextApply) = additionalTextLayout(TextNodeLayoutArguments(attributedString: additionalAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, lineSpacing: 0.0, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor)) var canExpand = false var clippedTextHeight: CGFloat = textLayout.size.height if textLayout.numberOfLines > 4 { - let (measuredTextLayout, _) = measureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 3, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor)) + let (measuredTextLayout, _) = measureTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 4, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets, lineColor: messageTheme.accentControlColor)) canExpand = true if !currentIsExpanded { @@ -340,7 +356,11 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let textFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: -textInsets.top + titleFrameWithoutInsets.height + textSpacing), size: textLayout.size) var textFrameWithoutInsets = CGRect(origin: CGPoint(x: textFrame.origin.x + textInsets.left, y: textFrame.origin.y + textInsets.top), size: CGSize(width: textFrame.width - textInsets.left - textInsets.right, height: clippedTextHeight - textInsets.top - textInsets.bottom)) textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) - + + let additionalTextFrame = CGRect(origin: CGPoint(x: titleFrame.origin.x, y: textFrame.maxY), size: additionalTextLayout.size) + var additionalTextFrameWithoutInsets = CGRect(origin: CGPoint(x: additionalTextFrame.origin.x + textInsets.left, y: additionalTextFrame.origin.y + textInsets.top), size: CGSize(width: additionalTextFrame.width - textInsets.left - textInsets.right, height: additionalTextFrame.height - textInsets.top - textInsets.bottom)) + additionalTextFrameWithoutInsets = additionalTextFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top) + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? if let statusType = statusType { var isReplyThread = false @@ -377,6 +397,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { suggestedBoundingWidth = max(suggestedBoundingWidth, statusSuggestedWidthAndContinue.0) } + suggestedBoundingWidth = max(suggestedBoundingWidth, additionalTextFrameWithoutInsets.width) let sideInsets = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right suggestedBoundingWidth += (sideInsets - 2.0) * 2.0 @@ -385,7 +406,13 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let statusSizeAndApply = statusSuggestedWidthAndContinue?.1(boundingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right) - boundingSize = CGSize(width: boundingWidth, height: topInset + titleFrameWithoutInsets.height + textFrameWithoutInsets.size.height + textSpacing) + var contentHeight = titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.size.height + if canExpand && !currentIsExpanded { + } else { + contentHeight += textSpacing * 2.0 + 1.0 + additionalTextFrameWithoutInsets.height + } + contentHeight += textSpacing + boundingSize = CGSize(width: boundingWidth, height: topInset + contentHeight - textSpacing) if let statusSizeAndApply = statusSizeAndApply { boundingSize.height += statusSizeAndApply.0.height } @@ -400,7 +427,8 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let themeUpdated = strongSelf.item?.presentationData.theme.theme !== item.presentationData.theme.theme strongSelf.item = item - + strongSelf.countryName = countryName + let backgroundView: MessageInlineBlockBackgroundView if let current = strongSelf.backgroundView { backgroundView = current @@ -410,6 +438,10 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode strongSelf.backgroundView = backgroundView } + if themeUpdated { + strongSelf.lineNode.backgroundColor = mainColor.withAlphaComponent(0.15) + } + var isExpandedUpdated = false if strongSelf.appliedIsExpanded != currentIsExpanded { strongSelf.appliedIsExpanded = currentIsExpanded @@ -464,57 +496,12 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode let _ = textApply() strongSelf.textNode.frame = CGRect(origin: .zero, size: textFrame.size) - - var clippingTextFrame = textFrame.offsetBy(dx: 0.0, dy: topInset) - clippingTextFrame.size.height = clippedTextHeight - 3.0 - if canExpand { - let wasHidden = strongSelf.expandIcon.isHidden - strongSelf.expandIcon.isHidden = false - if strongSelf.maskView?.image == nil { - strongSelf.maskView?.image = generateMaskImage() - } - strongSelf.textClippingNode.view.mask = strongSelf.maskView - - var expandIconFrame: CGRect = .zero - if let icon = strongSelf.expandIcon.image { - expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: clippingTextFrame.maxY - icon.size.height - 5.0), size: icon.size) - if wasHidden || isFirstTime { - strongSelf.expandIcon.position = expandIconFrame.center - } else { - animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil) - } - strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size) - } - - let expandButtonFrame = expandIconFrame.insetBy(dx: -8.0, dy: -8.0) - - let expandButton: HighlightTrackingButtonNode - if let current = strongSelf.expandButton { - expandButton = current - } else { - expandButton = HighlightTrackingButtonNode() - expandButton.addTarget(self, action: #selector(strongSelf.expandPressed), forControlEvents: .touchUpInside) - expandButton.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.expandIcon.layer.removeAnimation(forKey: "opacity") - strongSelf.expandIcon.alpha = 0.4 - } else { - strongSelf.expandIcon.alpha = 1.0 - strongSelf.expandIcon.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } - strongSelf.expandButton = expandButton - strongSelf.addSubnode(expandButton) - } - expandButton.frame = expandButtonFrame - } else { - strongSelf.expandIcon.isHidden = true - strongSelf.textClippingNode.view.mask = nil - } + let _ = additionalTextApply() + strongSelf.additionalTextNode.frame = CGRect(origin: CGPoint(x: 0.0, y: textFrame.height - textInsets.bottom + textSpacing + 1.0), size: additionalTextFrame.size) + let clippingTextFrame = CGRect(origin: textFrame.origin.offsetBy(dx: 0.0, dy: topInset), size: CGSize(width: boundingWidth, height: contentHeight - titleFrame.height + textSpacing)) + var titleLineWidth: CGFloat = 0.0 if let firstLine = titleLayout.linesRects().first { titleLineWidth = firstLine.width @@ -559,7 +546,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode titleBadgeButton.setBackgroundImage(generateFilledCircleImage(diameter: badgeBackgroundFrame.height, color: mainColor.withMultipliedAlpha(0.1))?.stretchableImage(withLeftCapWidth: Int(badgeBackgroundFrame.height / 2), topCapHeight: Int(badgeBackgroundFrame.height / 2)), for: .normal) } - let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top + topInset), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: titleFrameWithoutInsets.height + textSpacing + textFrameWithoutInsets.height + textSpacing)) + let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top + topInset), size: CGSize(width: boundingWidth - backgroundInsets.left - backgroundInsets.right, height: contentHeight)) if isFirstTime { strongSelf.textClippingNode.frame = clippingTextFrame @@ -578,6 +565,55 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode } backgroundView.update(size: backgroundFrame.size, isTransparent: false, primaryColor: mainColor, secondaryColor: nil, thirdColor: nil, backgroundColor: nil, pattern: nil, patternTopRightPosition: nil, animation: isFirstTime ? .None : animation) + animation.animator.updateFrame(layer: strongSelf.lineNode.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: textFrame.height - textSpacing + 1.0), size: CGSize(width: backgroundFrame.width - 9.0 - 6.0, height: 1.0 - UIScreenPixel)), completion: nil) + + if canExpand { + let wasHidden = strongSelf.expandIcon.isHidden + strongSelf.expandIcon.isHidden = false + if strongSelf.maskView?.image == nil { + strongSelf.maskView?.image = generateMaskImage() + } + strongSelf.textClippingNode.view.mask = strongSelf.maskView + + var expandIconFrame: CGRect = .zero + if let icon = strongSelf.expandIcon.image { + expandIconFrame = CGRect(origin: CGPoint(x: boundingWidth - icon.size.width - 19.0, y: backgroundFrame.maxY - icon.size.height - 6.0), size: icon.size) + if wasHidden || isFirstTime { + strongSelf.expandIcon.position = expandIconFrame.center + } else { + animation.animator.updatePosition(layer: strongSelf.expandIcon.layer, position: expandIconFrame.center, completion: nil) + } + strongSelf.expandIcon.bounds = CGRect(origin: .zero, size: expandIconFrame.size) + } + + let expandButtonFrame = expandIconFrame.insetBy(dx: -8.0, dy: -8.0) + + let expandButton: HighlightTrackingButtonNode + if let current = strongSelf.expandButton { + expandButton = current + } else { + expandButton = HighlightTrackingButtonNode() + expandButton.addTarget(self, action: #selector(strongSelf.expandPressed), forControlEvents: .touchUpInside) + expandButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.expandIcon.layer.removeAnimation(forKey: "opacity") + strongSelf.expandIcon.alpha = 0.4 + } else { + strongSelf.expandIcon.alpha = 1.0 + strongSelf.expandIcon.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + strongSelf.expandButton = expandButton + strongSelf.addSubnode(expandButton) + } + expandButton.frame = expandButtonFrame + } else { + strongSelf.expandIcon.isHidden = true + strongSelf.textClippingNode.view.mask = nil + } + if let statusSizeAndApply = statusSizeAndApply { strongSelf.statusNode.reactionSelected = { [weak strongSelf] _, value, sourceView in guard let strongSelf, let item = strongSelf.item else { @@ -594,7 +630,7 @@ public class ChatMessageFactCheckBubbleContentNode: ChatMessageBubbleContentNode item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) } - let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: topInset + textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) + let statusFrame = CGRect(origin: CGPoint(x: boundingWidth - layoutConstants.text.bubbleInsets.right - statusSizeAndApply.0.width, y: backgroundFrame.maxY + 4.0), size: statusSizeAndApply.0) if isFirstTime { strongSelf.statusNode.frame = statusFrame } else { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index b8becd4489..cd33ef2627 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -11770,7 +11770,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro hasBirthdayToday = true } - if hasBirthdayToday, let age = ageForBirthday(birthday), age > 0 { + if hasBirthdayToday { Queue.mainQueue().after(0.3) { var birthdayItemFrame: CGRect? if let section = self.regularSections[InfoSection.peerInfo] { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift index 81da4e62d2..67bbb7e785 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionScreen.swift @@ -29,19 +29,22 @@ private final class StarsTransactionSheetContent: CombinedComponent { let action: () -> Void let cancel: (Bool) -> Void let openPeer: (EnginePeer) -> Void + let copyTransactionId: () -> Void init( context: AccountContext, subject: StarsTransactionScreen.Subject, action: @escaping () -> Void, cancel: @escaping (Bool) -> Void, - openPeer: @escaping (EnginePeer) -> Void + openPeer: @escaping (EnginePeer) -> Void, + copyTransactionId: @escaping () -> Void ) { self.context = context self.subject = subject self.action = action self.cancel = cancel self.openPeer = openPeer + self.copyTransactionId = copyTransactionId } static func ==(lhs: StarsTransactionSheetContent, rhs: StarsTransactionSheetContent) -> Bool { @@ -163,7 +166,21 @@ private final class StarsTransactionSheetContent: CombinedComponent { let gloss = false switch subject { case let .transaction(transaction): - titleText = "Product Title" + switch transaction.peer { + case .peer: + titleText = "Product Title" + case .appStore: + titleText = "In-app Purchase" + case .playMarket: + titleText = "Play Market" + case .premiumBot: + titleText = "Premium Bot" + case .fragment: + titleText = "Fragment" + case .unsupported: + titleText = "Unsupported" + } + if transaction.count < 0 { descriptionText = "- \(transaction.count * -1) ⭐️" } else { @@ -185,7 +202,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { component: MultilineTextComponent( text: .plain(NSAttributedString( string: titleText, - font: Font.semibold(24.0), + font: Font.bold(25.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center )), @@ -211,16 +228,9 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: .immediate ) - let textFont = Font.regular(15.0) - let boldTextFont = Font.semibold(15.0) - let textColor = theme.actionSheet.primaryTextColor - let linkColor = theme.actionSheet.controlAccentColor - let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in - return (TelegramTextAttributes.URL, contents) - }) let description = description.update( component: BalancedTextComponent( - text: .plain(NSAttributedString(string: descriptionText, font: boldTextFont, textColor: descriptionText.hasPrefix("-") ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor)), + text: .plain(NSAttributedString(string: descriptionText, font: Font.semibold(17.0), textColor: descriptionText.hasPrefix("-") ? theme.list.itemDestructiveColor : theme.list.itemDisclosureActions.constructive.fillColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.2 @@ -240,7 +250,13 @@ private final class StarsTransactionSheetContent: CombinedComponent { title: strings.Stars_Transaction_Date, component: AnyComponent( Button( - content: AnyComponent(PeerCellComponent(context: context.component.context, textColor: tableLinkColor, peer: toPeer)), + content: AnyComponent( + PeerCellComponent( + context: component.context, + textColor: tableLinkColor, + peer: toPeer + ) + ), action: { if toPeer.id != accountContext.account.peerId { component.openPeer(toPeer) @@ -258,12 +274,16 @@ private final class StarsTransactionSheetContent: CombinedComponent { id: "transaction", title: strings.Stars_Transaction_Id, component: AnyComponent( - MultilineTextComponent( - text: .plain(NSAttributedString(string: transactionId, font: Font.monospace(15.0), textColor: tableTextColor)), - truncationType: .end, - maximumNumberOfLines: 0 + TransactionCellComponent( + textColor: tableTextColor, + accentColor: tableLinkColor, + transactionId: transactionId, + copy: { + component.copyTransactionId() + } ) - ) + ), + insets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 5.0) )) tableItems.append(.init( @@ -283,6 +303,13 @@ private final class StarsTransactionSheetContent: CombinedComponent { transition: .immediate ) + let textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + let textColor = theme.actionSheet.secondaryTextColor + let linkColor = theme.actionSheet.controlAccentColor + let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in + return (TelegramTextAttributes.URL, contents) + }) let additional = additional.update( component: BalancedTextComponent( text: .markdown(text: additionalText, attributes: markdownAttributes), @@ -319,16 +346,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { animationName: nil, iconPosition: .left, isLoading: state.inProgress, - action: { [weak state] in - if gloss { - component.action() - if let state { - state.inProgress = true - state.updated() - } - } else { - component.cancel(true) - } + action: { + component.cancel(true) } ), availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: 50.0), @@ -336,7 +355,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: 28.0 + 125.0)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: 31.0 + 125.0)) ) context.add(star @@ -344,12 +363,12 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) var originY: CGFloat = 0.0 - originY += star.size.height - 32.0 + originY += star.size.height - 23.0 context.add(description .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + description.size.height / 2.0)) ) - originY += description.size.height + 21.0 + originY += description.size.height + 20.0 context.add(table .position(CGPoint(x: context.availableSize.width / 2.0, y: originY + table.size.height / 2.0)) @@ -384,17 +403,20 @@ private final class StarsTransactionSheetComponent: CombinedComponent { let subject: StarsTransactionScreen.Subject let action: () -> Void let openPeer: (EnginePeer) -> Void + let copyTransactionId: () -> Void init( context: AccountContext, subject: StarsTransactionScreen.Subject, action: @escaping () -> Void, - openPeer: @escaping (EnginePeer) -> Void + openPeer: @escaping (EnginePeer) -> Void, + copyTransactionId: @escaping () -> Void ) { self.context = context self.subject = subject self.action = action self.openPeer = openPeer + self.copyTransactionId = copyTransactionId } static func ==(lhs: StarsTransactionSheetComponent, rhs: StarsTransactionSheetComponent) -> Bool { @@ -411,6 +433,8 @@ private final class StarsTransactionSheetComponent: CombinedComponent { 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 @@ -423,20 +447,23 @@ private final class StarsTransactionSheetComponent: CombinedComponent { action: context.component.action, cancel: { animate in if animate { - animateOut.invoke(Action { _ in - if let controller = controller() { - controller.dismiss(completion: nil) - } - }) + if let controller = controller() as? StarsTransactionScreen { + controller.dismissAllTooltips() + animateOut.invoke(Action { [weak controller] _ in + controller?.dismiss(completion: nil) + }) + } } else if let controller = controller() { controller.dismiss(animated: false, completion: nil) } }, - openPeer: context.component.openPeer + openPeer: context.component.openPeer, + copyTransactionId: context.component.copyTransactionId )), backgroundColor: .color(environment.theme.actionSheet.opaqueItemBackgroundColor), followContentSizeChanges: true, clipsContent: true, + externalState: sheetExternalState, animateOut: animateOut ), environment: { @@ -448,13 +475,15 @@ private final class StarsTransactionSheetComponent: CombinedComponent { regularMetricsSize: CGSize(width: 430.0, height: 900.0), dismiss: { animated in if animated { - animateOut.invoke(Action { _ in - if let controller = controller() { + if let controller = controller() as? StarsTransactionScreen { + controller.dismissAllTooltips() + animateOut.invoke(Action { _ in controller.dismiss(completion: nil) - } - }) + }) + } } else { - if let controller = controller() { + if let controller = controller() as? StarsTransactionScreen { + controller.dismissAllTooltips() controller.dismiss(completion: nil) } } @@ -469,6 +498,22 @@ private final class StarsTransactionSheetComponent: CombinedComponent { .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) ) + if let controller = controller(), !controller.automaticallyControlPresentationContextLayout { + let layout = ContainerViewLayout( + size: context.availableSize, + metrics: environment.metrics, + deviceMetrics: environment.deviceMetrics, + intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: max(environment.safeInsets.bottom, sheetExternalState.contentHeight), right: 0.0), + safeInsets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: 0.0, right: 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 } } @@ -493,6 +538,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { self.context = context var openPeerImpl: ((EnginePeer) -> Void)? + var copyTransactionIdImpl: (() -> Void)? super.init( context: context, component: StarsTransactionSheetComponent( @@ -501,6 +547,9 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { action: action, openPeer: { peerId in openPeerImpl?(peerId) + }, + copyTransactionId: { + copyTransactionIdImpl?() } ), navigationBarAppearance: .none, @@ -509,11 +558,14 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { ) self.navigationPresentation = .flatModal + self.automaticallyControlPresentationContextLayout = false openPeerImpl = { [weak self] peer in guard let self, let navigationController = self.navigationController as? NavigationController else { return } + self.dismissAllTooltips() + let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id) ) @@ -524,6 +576,16 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(peer), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: nil, animated: true)) }) } + + copyTransactionIdImpl = { [weak self] in + guard let self else { + return + } + self.dismissAllTooltips() + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Stars_Transaction_CopiedId), elevatedLayout: false, position: .bottom, action: { _ in return true }), in: .current) + } } required public init(coder aDecoder: NSCoder) { @@ -546,6 +608,8 @@ public class StarsTransactionScreen: ViewControllerComponentContainer { } public func dismissAnimated() { + self.dismissAllTooltips() + if let view = self.node.hostView.findTaggedView(tag: SheetComponent.View.Tag()) as? SheetComponent.View { view.dismissAnimated() } @@ -571,11 +635,13 @@ private final class TableComponent: CombinedComponent { public let id: AnyHashable public let title: String public let component: AnyComponent + public let insets: UIEdgeInsets? - public init(id: IdType, title: String, component: AnyComponent) { + public init(id: IdType, title: String, component: AnyComponent, insets: UIEdgeInsets? = nil) { self.id = AnyHashable(id) self.title = title self.component = component + self.insets = insets } public static func == (lhs: Item, rhs: Item) -> Bool { @@ -588,6 +654,9 @@ private final class TableComponent: CombinedComponent { if lhs.component != rhs.component { return false } + if lhs.insets != rhs.insets { + return false + } return true } } @@ -637,7 +706,7 @@ private final class TableComponent: CombinedComponent { var leftColumnWidth: CGFloat = 0.0 var updatedTitleChildren: [_UpdatedChildComponent] = [] - var updatedValueChildren: [_UpdatedChildComponent] = [] + var updatedValueChildren: [(_UpdatedChildComponent, UIEdgeInsets)] = [] var updatedBorderChildren: [_UpdatedChildComponent] = [] for item in context.component.items { @@ -664,12 +733,19 @@ private final class TableComponent: CombinedComponent { for item in context.component.items { let titleChild = updatedTitleChildren[i] + + let insets: UIEdgeInsets + if let customInsets = item.insets { + insets = customInsets + } else { + insets = UIEdgeInsets(top: 0.0, left: horizontalPadding, bottom: 0.0, right: horizontalPadding) + } let valueChild = valueChildren[item.id].update( component: item.component, - availableSize: CGSize(width: rightColumnWidth - horizontalPadding * 2.0, height: context.availableSize.height), + availableSize: CGSize(width: rightColumnWidth - insets.left - insets.right, height: context.availableSize.height), transition: context.transition ) - updatedValueChildren.append(valueChild) + updatedValueChildren.append((valueChild, insets)) let rowHeight = max(40.0, max(titleChild.size.height, valueChild.size.height) + verticalPadding * 2.0) rowHeights[i] = rowHeight @@ -742,11 +818,11 @@ private final class TableComponent: CombinedComponent { i = 0 var originY: CGFloat = 0.0 - for (titleChild, valueChild) in zip(updatedTitleChildren, updatedValueChildren) { + for (titleChild, (valueChild, valueInsets)) in zip(updatedTitleChildren, updatedValueChildren) { let rowHeight = rowHeights[i] ?? 0.0 let titleFrame = CGRect(origin: CGPoint(x: horizontalPadding, y: originY + verticalPadding), size: titleChild.size) - let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + horizontalPadding, y: originY + verticalPadding), size: valueChild.size) + let valueFrame = CGRect(origin: CGPoint(x: leftColumnWidth + valueInsets.left, y: originY + verticalPadding), size: valueChild.size) context.add(titleChild .position(titleFrame.center) @@ -866,6 +942,113 @@ private final class PeerCellComponent: Component { } } +private final class TransactionCellComponent: Component { + let textColor: UIColor + let accentColor: UIColor + let transactionId: String + let copy: () -> Void + + init(textColor: UIColor, accentColor: UIColor, transactionId: String, copy: @escaping () -> Void) { + self.textColor = textColor + self.accentColor = accentColor + self.transactionId = transactionId + self.copy = copy + } + + static func ==(lhs: TransactionCellComponent, rhs: TransactionCellComponent) -> Bool { + if lhs.textColor !== rhs.textColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.transactionId != rhs.transactionId { + return false + } + return true + } + + final class View: UIView { + private let text = ComponentView() + private let button = ComponentView() + + private var component: TransactionCellComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: TransactionCellComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let spacing: CGFloat = 6.0 + + let buttonSize = self.button.update( + transition: .immediate, + component: AnyComponent( + Button( + content: AnyComponent( + BundleIconComponent(name: "Chat/Context Menu/Copy", tintColor: component.accentColor) + ), + action: { + component.copy() + } + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height) + ) + + let textSize = self.text.update( + transition: .immediate, + component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: component.transactionId, font: Font.monospace(15.0), textColor: component.textColor, paragraphAlignment: .left)), + maximumNumberOfLines: 0, + lineSpacing: 0.2 + ) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - buttonSize.width - spacing, height: availableSize.height) + ) + + let size = CGSize(width: textSize.width + spacing + buttonSize.width, height: textSize.height) + + + let buttonFrame = CGRect(origin: CGPoint(x: textSize.width + spacing, y: floorToScreenPixels((size.height - buttonSize.height) / 2.0)), size: buttonSize) + if let buttonView = self.button.view { + if buttonView.superview == nil { + self.addSubview(buttonView) + } + transition.setFrame(view: buttonView, frame: buttonFrame) + } + + let textFrame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - textSize.height) / 2.0)), size: textSize) + if let textView = self.text.view { + if textView.superview == nil { + self.addSubview(textView) + } + transition.setFrame(view: textView, frame: textFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} private func generateCloseButtonImage(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in diff --git a/versions.json b/versions.json index f069c6c557..5f83936904 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.12", + "app": "10.13", "xcode": "15.2", "bazel": "7.1.1", "macos": "13.0"