diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 74cf0c9035..16e056f9b0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -14202,3 +14202,21 @@ Sorry for the inconvenience."; "Story.Privacy.KeepOnMyPageManyInfo" = "Keep these stories on your profile even after they expire in %@. Privacy settings will apply."; "Story.Privacy.KeepOnChannelPageManyInfo" = "Keep these stories on the channel profile even after they expire in %@."; "Story.Privacy.KeepOnGroupPageManyInfo" = "Keep these stories on the group page even after they expire in %@."; + +"Gift.Options.Gift.Filter.Resale" = "Resale"; +"Gift.Options.Gift.Resale" = "resale"; + +"Stars.Intro.Transaction.GiftPurchase" = "Gift Purchase"; +"Stars.Intro.Transaction.GiftSale" = "Gift Sale"; + +"Stars.Transaction.GiftPurchase" = "Gift Purchase"; +"Stars.Transaction.GiftSale" = "Gift Sale"; + +"Channel.Info.AutoTranslate" = "Auto-Translate Messages"; + +"ChannelBoost.Table.AutoTranslate" = "Autotranslation of Messages"; +"ChannelBoost.AutoTranslate" = "Autotranslation of Messages"; +"ChannelBoost.AutoTranslateLevelText" = "Your channel needs **Level %1$@** to enable autotranslation of messages."; + +"Channel.AdminLog.MessageToggleAutoTranslateOn" = "%@ enabled autotranslation of messages"; +"Channel.AdminLog.MessageToggleAutoTranslateOff" = "%@ disabled autotranslation of messages"; diff --git a/submodules/AccountContext/Sources/Premium.swift b/submodules/AccountContext/Sources/Premium.swift index 790a21b077..2dea082727 100644 --- a/submodules/AccountContext/Sources/Premium.swift +++ b/submodules/AccountContext/Sources/Premium.swift @@ -125,6 +125,7 @@ public enum BoostSubject: Equatable { case emojiPack case noAds case wearGift + case autoTranslate } public enum StarsPurchasePurpose: Equatable { @@ -164,6 +165,7 @@ public struct PremiumConfiguration { minChannelCustomWallpaperLevel: 10, minChannelRestrictAdsLevel: 50, minChannelWearGiftLevel: 8, + minChannelAutoTranslateLevel: 3, minGroupProfileIconLevel: 7, minGroupEmojiStatusLevel: 8, minGroupWallpaperLevel: 9, @@ -193,6 +195,7 @@ public struct PremiumConfiguration { public let minChannelCustomWallpaperLevel: Int32 public let minChannelRestrictAdsLevel: Int32 public let minChannelWearGiftLevel: Int32 + public let minChannelAutoTranslateLevel: Int32 public let minGroupProfileIconLevel: Int32 public let minGroupEmojiStatusLevel: Int32 public let minGroupWallpaperLevel: Int32 @@ -221,6 +224,7 @@ public struct PremiumConfiguration { minChannelCustomWallpaperLevel: Int32, minChannelRestrictAdsLevel: Int32, minChannelWearGiftLevel: Int32, + minChannelAutoTranslateLevel: Int32, minGroupProfileIconLevel: Int32, minGroupEmojiStatusLevel: Int32, minGroupWallpaperLevel: Int32, @@ -248,6 +252,7 @@ public struct PremiumConfiguration { self.minChannelCustomWallpaperLevel = minChannelCustomWallpaperLevel self.minChannelRestrictAdsLevel = minChannelRestrictAdsLevel self.minChannelWearGiftLevel = minChannelWearGiftLevel + self.minChannelAutoTranslateLevel = minChannelAutoTranslateLevel self.minGroupProfileIconLevel = minGroupProfileIconLevel self.minGroupEmojiStatusLevel = minGroupEmojiStatusLevel self.minGroupWallpaperLevel = minGroupWallpaperLevel @@ -283,6 +288,7 @@ public struct PremiumConfiguration { minChannelCustomWallpaperLevel: get(data["channel_custom_wallpaper_level_min"]) ?? defaultValue.minChannelCustomWallpaperLevel, minChannelRestrictAdsLevel: get(data["channel_restrict_sponsored_level_min"]) ?? defaultValue.minChannelRestrictAdsLevel, minChannelWearGiftLevel: get(data["channel_emoji_status_level_min"]) ?? defaultValue.minChannelWearGiftLevel, + minChannelAutoTranslateLevel: get(data["channel_autotranslation_level_min"]) ?? defaultValue.minChannelAutoTranslateLevel, minGroupProfileIconLevel: get(data["group_profile_bg_icon_level_min"]) ?? defaultValue.minGroupProfileIconLevel, minGroupEmojiStatusLevel: get(data["group_emoji_status_level_min"]) ?? defaultValue.minGroupEmojiStatusLevel, minGroupWallpaperLevel: get(data["group_wallpaper_level_min"]) ?? defaultValue.minGroupWallpaperLevel, diff --git a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift index aae7f44fe6..d9c5782795 100644 --- a/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumBoostLevelsScreen.swift @@ -61,6 +61,8 @@ func requiredBoostSubjectLevel(subject: BoostSubject, group: Bool, context: Acco return configuration.minChannelRestrictAdsLevel case .wearGift: return configuration.minChannelWearGiftLevel + case .autoTranslate: + return configuration.minChannelAutoTranslateLevel } } @@ -243,6 +245,7 @@ private final class LevelSectionComponent: CombinedComponent { case emojiPack case noAds case wearGift + case autoTranslate func title(strings: PresentationStrings, isGroup: Bool) -> String { switch self { @@ -274,6 +277,8 @@ private final class LevelSectionComponent: CombinedComponent { return strings.ChannelBoost_Table_NoAds case .wearGift: return strings.ChannelBoost_Table_WearGift + case .autoTranslate: + return strings.ChannelBoost_Table_AutoTranslate } } @@ -307,6 +312,8 @@ private final class LevelSectionComponent: CombinedComponent { return "Premium/BoostPerk/NoAds" case .wearGift: return "Premium/BoostPerk/NoAds" + case .autoTranslate: + return "Chat/Title Panels/Translate" } } } @@ -647,6 +654,8 @@ private final class SheetContent: CombinedComponent { textString = strings.ChannelBoost_EnableNoAdsLevelText("\(requiredLevel)").string case .wearGift: textString = strings.ChannelBoost_WearGiftLevelText("\(requiredLevel)").string + case .autoTranslate: + textString = strings.ChannelBoost_AutoTranslateLevelText("\(requiredLevel)").string } } else { let boostsString = strings.ChannelBoost_MoreBoostsNeeded_Boosts(Int32(remaining)) @@ -1162,6 +1171,9 @@ private final class SheetContent: CombinedComponent { if !isGroup && level >= requiredBoostSubjectLevel(subject: .noAds, group: isGroup, context: component.context, configuration: premiumConfiguration) { perks.append(.noAds) } + if !isGroup && level >= requiredBoostSubjectLevel(subject: .autoTranslate, group: isGroup, context: component.context, configuration: premiumConfiguration) { + perks.append(.autoTranslate) + } // if !isGroup && level >= requiredBoostSubjectLevel(subject: .wearGift, group: isGroup, context: component.context, configuration: premiumConfiguration) { // perks.append(.wearGift) // } @@ -1466,6 +1478,8 @@ private final class BoostLevelsContainerComponent: CombinedComponent { titleString = strings.ChannelBoost_NoAds case .wearGift: titleString = strings.ChannelBoost_WearGift + case .autoTranslate: + titleString = strings.ChannelBoost_AutoTranslate } } else { titleString = isGroup == true ? strings.GroupBoost_Title_Current : strings.ChannelBoost_Title_Current diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index a3ffedec81..6d25bfee71 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -171,6 +171,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[589338437] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStartGroupCall($0) } dict[-1895328189] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionStopPoll($0) } dict[1693675004] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleAntiSpam($0) } + dict[-988285058] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleAutotranslation($0) } dict[46949251] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleForum($0) } dict[1456906823] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleGroupCallSetting($0) } dict[460916654] = { return Api.ChannelAdminLogEventAction.parse_channelAdminLogEventActionToggleInvites($0) } @@ -1461,6 +1462,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[276907596] = { return Api.storage.FileType.parse_fileWebp($0) } dict[1862033025] = { return Api.stories.AllStories.parse_allStories($0) } dict[291044926] = { return Api.stories.AllStories.parse_allStoriesNotModified($0) } + dict[-1014513586] = { return Api.stories.CanSendStoryCount.parse_canSendStoryCount($0) } dict[-488736969] = { return Api.stories.FoundStories.parse_foundStories($0) } dict[-890861720] = { return Api.stories.PeerStories.parse_peerStories($0) } dict[1673780490] = { return Api.stories.Stories.parse_stories($0) } @@ -2592,6 +2594,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.stories.AllStories: _1.serialize(buffer, boxed) + case let _1 as Api.stories.CanSendStoryCount: + _1.serialize(buffer, boxed) case let _1 as Api.stories.FoundStories: _1.serialize(buffer, boxed) case let _1 as Api.stories.PeerStories: diff --git a/submodules/TelegramApi/Sources/Api3.swift b/submodules/TelegramApi/Sources/Api3.swift index b6ec88ab5b..1818e0060b 100644 --- a/submodules/TelegramApi/Sources/Api3.swift +++ b/submodules/TelegramApi/Sources/Api3.swift @@ -605,6 +605,7 @@ public extension Api { case channelAdminLogEventActionStartGroupCall(call: Api.InputGroupCall) case channelAdminLogEventActionStopPoll(message: Api.Message) case channelAdminLogEventActionToggleAntiSpam(newValue: Api.Bool) + case channelAdminLogEventActionToggleAutotranslation(newValue: Api.Bool) case channelAdminLogEventActionToggleForum(newValue: Api.Bool) case channelAdminLogEventActionToggleGroupCallSetting(joinMuted: Api.Bool) case channelAdminLogEventActionToggleInvites(newValue: Api.Bool) @@ -897,6 +898,12 @@ public extension Api { } newValue.serialize(buffer, true) break + case .channelAdminLogEventActionToggleAutotranslation(let newValue): + if boxed { + buffer.appendInt32(-988285058) + } + newValue.serialize(buffer, true) + break case .channelAdminLogEventActionToggleForum(let newValue): if boxed { buffer.appendInt32(46949251) @@ -1039,6 +1046,8 @@ public extension Api { return ("channelAdminLogEventActionStopPoll", [("message", message as Any)]) case .channelAdminLogEventActionToggleAntiSpam(let newValue): return ("channelAdminLogEventActionToggleAntiSpam", [("newValue", newValue as Any)]) + case .channelAdminLogEventActionToggleAutotranslation(let newValue): + return ("channelAdminLogEventActionToggleAutotranslation", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleForum(let newValue): return ("channelAdminLogEventActionToggleForum", [("newValue", newValue as Any)]) case .channelAdminLogEventActionToggleGroupCallSetting(let joinMuted): @@ -1677,6 +1686,19 @@ public extension Api { return nil } } + public static func parse_channelAdminLogEventActionToggleAutotranslation(_ reader: BufferReader) -> ChannelAdminLogEventAction? { + var _1: Api.Bool? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.Bool + } + let _c1 = _1 != nil + if _c1 { + return Api.ChannelAdminLogEventAction.channelAdminLogEventActionToggleAutotranslation(newValue: _1!) + } + else { + return nil + } + } public static func parse_channelAdminLogEventActionToggleForum(_ reader: BufferReader) -> ChannelAdminLogEventAction? { var _1: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramApi/Sources/Api36.swift b/submodules/TelegramApi/Sources/Api36.swift index 804687918d..e3c0df82da 100644 --- a/submodules/TelegramApi/Sources/Api36.swift +++ b/submodules/TelegramApi/Sources/Api36.swift @@ -760,6 +760,42 @@ public extension Api.stories { } } +public extension Api.stories { + enum CanSendStoryCount: TypeConstructorDescription { + case canSendStoryCount(countRemains: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .canSendStoryCount(let countRemains): + if boxed { + buffer.appendInt32(-1014513586) + } + serializeInt32(countRemains, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .canSendStoryCount(let countRemains): + return ("canSendStoryCount", [("countRemains", countRemains as Any)]) + } + } + + public static func parse_canSendStoryCount(_ reader: BufferReader) -> CanSendStoryCount? { + var _1: Int32? + _1 = reader.readInt32() + let _c1 = _1 != nil + if _c1 { + return Api.stories.CanSendStoryCount.canSendStoryCount(countRemains: _1!) + } + else { + return nil + } + } + + } +} public extension Api.stories { enum FoundStories: TypeConstructorDescription { case foundStories(flags: Int32, count: Int32, stories: [Api.FoundStory], nextOffset: String?, chats: [Api.Chat], users: [Api.User]) @@ -1560,55 +1596,3 @@ public extension Api.updates { } } -public extension Api.updates { - enum State: TypeConstructorDescription { - case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - if boxed { - buffer.appendInt32(-1519637954) - } - serializeInt32(pts, buffer: buffer, boxed: false) - serializeInt32(qts, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - serializeInt32(seq, buffer: buffer, boxed: false) - serializeInt32(unreadCount, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .state(let pts, let qts, let date, let seq, let unreadCount): - return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) - } - } - - public static func parse_state(_ reader: BufferReader) -> State? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - var _3: Int32? - _3 = reader.readInt32() - var _4: Int32? - _4 = reader.readInt32() - var _5: Int32? - _5 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 { - return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api37.swift b/submodules/TelegramApi/Sources/Api37.swift index e76c07649a..281a495dbf 100644 --- a/submodules/TelegramApi/Sources/Api37.swift +++ b/submodules/TelegramApi/Sources/Api37.swift @@ -1,3 +1,55 @@ +public extension Api.updates { + enum State: TypeConstructorDescription { + case state(pts: Int32, qts: Int32, date: Int32, seq: Int32, unreadCount: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + if boxed { + buffer.appendInt32(-1519637954) + } + serializeInt32(pts, buffer: buffer, boxed: false) + serializeInt32(qts, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + serializeInt32(seq, buffer: buffer, boxed: false) + serializeInt32(unreadCount, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .state(let pts, let qts, let date, let seq, let unreadCount): + return ("state", [("pts", pts as Any), ("qts", qts as Any), ("date", date as Any), ("seq", seq as Any), ("unreadCount", unreadCount as Any)]) + } + } + + public static func parse_state(_ reader: BufferReader) -> State? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + var _3: Int32? + _3 = reader.readInt32() + var _4: Int32? + _4 = reader.readInt32() + var _5: Int32? + _5 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 { + return Api.updates.State.state(pts: _1!, qts: _2!, date: _3!, seq: _4!, unreadCount: _5!) + } + else { + return nil + } + } + + } +} public extension Api.upload { enum CdnFile: TypeConstructorDescription { case cdnFile(bytes: Buffer) diff --git a/submodules/TelegramApi/Sources/Api38.swift b/submodules/TelegramApi/Sources/Api38.swift index 35b5eecc84..f937d610ca 100644 --- a/submodules/TelegramApi/Sources/Api38.swift +++ b/submodules/TelegramApi/Sources/Api38.swift @@ -3590,6 +3590,22 @@ public extension Api.functions.channels { }) } } +public extension Api.functions.channels { + static func toggleAutotranslation(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(377471137) + channel.serialize(buffer, true) + enabled.serialize(buffer, true) + return (FunctionDescription(name: "channels.toggleAutotranslation", parameters: [("channel", String(describing: channel)), ("enabled", String(describing: enabled))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} public extension Api.functions.channels { static func toggleForum(channel: Api.InputChannel, enabled: Api.Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -11139,15 +11155,15 @@ public extension Api.functions.stories { } } public extension Api.functions.stories { - static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func canSendStory(peer: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-941629475) + buffer.appendInt32(820732912) peer.serialize(buffer, true) - return (FunctionDescription(name: "stories.canSendStory", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + return (FunctionDescription(name: "stories.canSendStory", parameters: [("peer", String(describing: peer))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.stories.CanSendStoryCount? in let reader = BufferReader(buffer) - var result: Api.Bool? + var result: Api.stories.CanSendStoryCount? if let signature = reader.readInt32() { - result = Api.parse(reader, signature: signature) as? Api.Bool + result = Api.parse(reader, signature: signature) as? Api.stories.CanSendStoryCount } return result }) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index cfaf228f4f..d3954aecf4 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -1702,7 +1702,7 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor } public enum StoriesUploadAvailability { - case available + case available(remainingCount: Int32) case weeklyLimit case monthlyLimit case expiringLimit @@ -1729,10 +1729,9 @@ func _internal_checkStoriesUploadAvailability(account: Account, target: Stories. return account.network.request(Api.functions.stories.canSendStory(peer: inputPeer)) |> map { result -> StoriesUploadAvailability in - if result == .boolTrue { - return .available - } else { - return .unknownLimit + switch result { + case let .canSendStoryCount(countRemains): + return .available(remainingCount: countRemains) } } |> `catch` { error -> Signal in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 9abcd3c23f..9330c8db39 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -179,6 +179,7 @@ public enum BotPaymentFormRequestError { case alreadyActive case noPaymentNeeded case disallowedStarGift + case starGiftResellTooEarly(Int32) } extension BotPaymentInvoice { @@ -482,6 +483,11 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw return .fail(.noPaymentNeeded) } else if error.errorDescription == "USER_DISALLOWED_STARGIFTS" { return .fail(.disallowedStarGift) + } else if error.errorDescription.hasPrefix("STARGIFT_RESELL_TOO_EARLY_") { + let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "STARGIFT_RESELL_TOO_EARLY_".count)...]) + if let value = Int32(timeout) { + return .fail(.starGiftResellTooEarly(value)) + } } return .fail(.generic) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 73c0fffe67..138a3a7b22 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -847,8 +847,14 @@ public enum TransferStarGiftError { public enum BuyStarGiftError { case generic + case starGiftResellTooEarly(Int32) } +public enum UpdateStarGiftPriceError { + case generic +} + + public enum UpgradeStarGiftError { case generic } @@ -858,7 +864,12 @@ func _internal_buyStarGift(account: Account, slug: String, peerId: EnginePeer.Id return _internal_fetchBotPaymentForm(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, source: source, themeParams: nil) |> map(Optional.init) |> `catch` { error -> Signal in - return .fail(.generic) + switch error { + case let .starGiftResellTooEarly(value): + return .fail(.starGiftResellTooEarly(value)) + default: + return .fail(.generic) + } } |> mapToSignal { paymentForm in if let paymentForm { @@ -1487,7 +1498,13 @@ private final class ProfileGiftsContextImpl { } let disposable = MetaDisposable() disposable.set( - _internal_upgradeStarGift(account: self.account, formId: formId, reference: reference, keepOriginalInfo: keepOriginalInfo).startStrict(next: { [weak self] result in + (_internal_upgradeStarGift( + account: self.account, + formId: formId, + reference: reference, + keepOriginalInfo: keepOriginalInfo + ) + |> deliverOn(self.queue)).startStrict(next: { [weak self] result in guard let self else { return } @@ -1509,39 +1526,54 @@ private final class ProfileGiftsContextImpl { } } - func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) { - self.actionDisposable.set( - _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price).startStrict() - ) - - - if let index = self.gifts.firstIndex(where: { gift in - if gift.reference == reference { - return true - } - return false - }) { - if case let .unique(uniqueGift) = self.gifts[index].gift { - let updatedUniqueGift = uniqueGift.withResellStars(price) - let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)) - self.gifts[index] = updatedGift + func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) -> Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable } + let disposable = MetaDisposable() + disposable.set( + (_internal_updateStarGiftResalePrice( + account: self.account, + reference: reference, + price: price + ) + |> deliverOn(self.queue)).startStrict(error: { error in + subscriber.putError(error) + }, completed: { + if let index = self.gifts.firstIndex(where: { gift in + if gift.reference == reference { + return true + } + return false + }) { + if case let .unique(uniqueGift) = self.gifts[index].gift { + let updatedUniqueGift = uniqueGift.withResellStars(price) + let updatedGift = self.gifts[index].withGift(.unique(updatedUniqueGift)) + self.gifts[index] = updatedGift + } + } + + if let index = self.filteredGifts.firstIndex(where: { gift in + if gift.reference == reference { + return true + } + return false + }) { + if case let .unique(uniqueGift) = self.filteredGifts[index].gift { + let updatedUniqueGift = uniqueGift.withResellStars(price) + let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)) + self.filteredGifts[index] = updatedGift + } + } + + self.pushState() + + subscriber.putCompletion() + }) + ) + return disposable } - - if let index = self.filteredGifts.firstIndex(where: { gift in - if gift.reference == reference { - return true - } - return false - }) { - if case let .unique(uniqueGift) = self.filteredGifts[index].gift { - let updatedUniqueGift = uniqueGift.withResellStars(price) - let updatedGift = self.filteredGifts[index].withGift(.unique(updatedUniqueGift)) - self.filteredGifts[index] = updatedGift - } - } - - self.pushState() } func toggleStarGiftsNotifications(enabled: Bool) { @@ -1939,9 +1971,17 @@ public final class ProfileGiftsContext { } } - public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) { - self.impl.with { impl in - impl.updateStarGiftResellPrice(reference: reference, price: price) + public func updateStarGiftResellPrice(reference: StarGiftReference, price: Int64?) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.updateStarGiftResellPrice(reference: reference, price: price).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable } } @@ -2274,23 +2314,21 @@ func _internal_toggleStarGiftsNotifications(account: Account, peerId: EnginePeer } } -func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal { +func _internal_updateStarGiftResalePrice(account: Account, reference: StarGiftReference, price: Int64?) -> Signal { return account.postbox.transaction { transaction in return reference.apiStarGiftReference(transaction: transaction) } + |> castError(UpdateStarGiftPriceError.self) |> mapToSignal { starGift in guard let starGift else { return .complete() } return account.network.request(Api.functions.payments.updateStarGiftPrice(stargift: starGift, resellStars: price ?? 0)) - |> map(Optional.init) - |> `catch` { _ -> Signal in - return .single(nil) + |> mapError { error -> UpdateStarGiftPriceError in + return .generic } - |> mapToSignal { updates -> Signal in - if let updates { - account.stateManager.addUpdates(updates) - } + |> mapToSignal { updates -> Signal in + account.stateManager.addUpdates(updates) return .complete() } |> ignoreValues @@ -2496,6 +2534,66 @@ private final class ResaleGiftsContextImpl { self.loadMore() } + + func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal { + return _internal_buyStarGift(account: self.account, slug: slug, peerId: peerId) + |> afterCompleted { [weak self] in + guard let self else { + return + } + self.queue.async { + if let count = self.count { + self.count = max(0, count - 1) + } + self.gifts.removeAll(where: { gift in + if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug { + return true + } + return false + }) + self.pushState() + } + } + } + + func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal { + return Signal { [weak self] subscriber in + guard let self else { + return EmptyDisposable + } + let disposable = MetaDisposable() + disposable.set( + (_internal_updateStarGiftResalePrice( + account: self.account, + reference: .slug(slug: slug), + price: price + ) + |> deliverOn(self.queue)).startStrict(error: { error in + subscriber.putError(error) + }, completed: { + if let index = self.gifts.firstIndex(where: { gift in + if case let .unique(uniqueGift) = gift, uniqueGift.slug == slug { + return true + } + return false + }) { + if let price { + if case let .unique(uniqueGift) = self.gifts[index] { + self.gifts[index] = .unique(uniqueGift.withResellStars(price)) + } + } else { + self.gifts.remove(at: index) + } + } + + self.pushState() + + subscriber.putCompletion() + }) + ) + return disposable + } + } private func pushState() { let state = ResaleGiftsContext.State( @@ -2584,6 +2682,34 @@ public final class ResaleGiftsContext { impl.updateFilterAttributes(attributes) } } + + public func buyStarGift(slug: String, peerId: EnginePeer.Id) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.buyStarGift(slug: slug, peerId: peerId).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } + + public func updateStarGiftResellPrice(slug: String, price: Int64?) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.updateStarGiftResellPrice(slug: slug, price: price).start(error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + } + return disposable + } + } public var currentState: ResaleGiftsContext.State? { var state: ResaleGiftsContext.State? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index 87b914ce58..2fcf1f873a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -153,7 +153,7 @@ public extension TelegramEngine { return _internal_toggleStarGiftsNotifications(account: self.account, peerId: peerId, enabled: enabled) } - public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal { + public func updateStarGiftResalePrice(reference: StarGiftReference, price: Int64?) -> Signal { return _internal_updateStarGiftResalePrice(account: self.account, reference: reference, price: price) } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift index 39e07ac740..6937e7b94a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift @@ -94,6 +94,7 @@ public enum AdminLogEventAction { case changeStatus(prev: PeerEmojiStatus?, new: PeerEmojiStatus?) case changeEmojiPack(prev: StickerPackReference?, new: StickerPackReference?) case participantSubscriptionExtended(prev: RenderedChannelParticipant, new: RenderedChannelParticipant) + case toggleAutoTranslation(Bool) } public enum ChannelAdminLogEventError { @@ -457,6 +458,8 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] { action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer)) } + case let .channelAdminLogEventActionToggleAutotranslation(newValue): + action = .toggleAutoTranslation(boolFromApiValue(newValue)) } let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)) if let action = action { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index f2cf4d1cd2..3bd97fb244 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -3469,7 +3469,11 @@ public class CameraScreenImpl: ViewController, CameraScreen { } self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] availability in - guard let self, availability != .available else { + guard let self else { + return + } + if case let .available(remainingCount) = availability { + let _ = remainingCount return } self.node.postingAvailable = false diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 8b3855f064..30cff91d9b 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -2282,6 +2282,33 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) + case let .toggleAutoTranslation(value): + var peers = SimpleDictionary() + var author: Peer? + if let peer = self.entry.peers[self.entry.event.peerId] { + author = peer + peers[peer.id] = peer + } + var text: String = "" + var entities: [MessageTextEntity] = [] + if value { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOn(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } else { + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageToggleAutoTranslateOff(author.flatMap(EnginePeer.init)?.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder) ?? ""), generateEntities: { index in + if index == 0, let author = author { + return [.TextMention(peerId: author.id)] + } + return [] + }, to: &text, entities: &entities) + } + let action = TelegramMediaActionType.customText(text: text, entities: entities, additionalAttributes: nil) + let message = Message(stableId: self.entry.stableId, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(bitPattern: self.entry.stableId)), globallyUniqueId: self.entry.event.id, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: self.entry.event.date, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: author, text: "", attributes: [], media: [TelegramMediaAction(action: action)], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) + return ChatMessageItemImpl(presentationData: self.presentationData, context: context, chatLocation: .peer(id: peer.id), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .channel, automaticDownloadPeerId: nil, automaticDownloadNetworkType: .cellular, isRecentActions: true, availableReactions: availableReactions, availableMessageEffects: availableMessageEffects, savedMessageTags: nil, defaultReaction: nil, isPremium: false, accountPeer: nil), controllerInteraction: controllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes(), location: nil)) } } } diff --git a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift index b327c778ce..52bd22d3ee 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftOptionsScreen/Sources/GiftOptionsScreen.swift @@ -370,18 +370,10 @@ final class GiftOptionsScreenComponent: Component { var isSoldOut = false switch gift { case let .generic(gift): - if let availability = gift.availability, availability.resale > 0 { - //TODO:localize - //TODO:unmock - ribbon = GiftItemComponent.Ribbon( - text: "resale", - color: .green - ) - } else if let _ = gift.soldOut { + if let _ = gift.soldOut { if let availability = gift.availability, availability.resale > 0 { - //TODO:localize ribbon = GiftItemComponent.Ribbon( - text: "resale", + text: environment.strings.Gift_Options_Gift_Resale, color: .green ) } else { @@ -415,7 +407,7 @@ final class GiftOptionsScreenComponent: Component { let subject: GiftItemComponent.Subject switch gift { case let .generic(gift): - if let availability = gift.availability, let minResaleStars = availability.minResaleStars { + if let availability = gift.availability, availability.remains == 0, let minResaleStars = availability.minResaleStars { subject = .starGift(gift: gift, price: "⭐️ \(minResaleStars)+") } else { subject = .starGift(gift: gift, price: "⭐️ \(gift.price)") @@ -450,7 +442,7 @@ final class GiftOptionsScreenComponent: Component { mainController = controller } if case let .generic(gift) = gift { - if let availability = gift.availability, availability.remains == 0 || (availability.resale > 0) { + if let availability = gift.availability, availability.remains == 0 { if availability.resale > 0 { let storeController = component.context.sharedContext.makeGiftStoreController( context: component.context, @@ -1296,7 +1288,7 @@ final class GiftOptionsScreenComponent: Component { starsAmountsSet.insert(gift.price) if let availability = gift.availability { hasLimited = true - if availability.resale > 0 { + if availability.remains == 0 && availability.resale > 0 { hasResale = true } } @@ -1317,10 +1309,9 @@ final class GiftOptionsScreenComponent: Component { )) if hasResale { - //TODO:localize tabSelectorItems.append(TabSelectorComponent.Item( id: AnyHashable(StarsFilter.resale.rawValue), - title: "Resale" + title: strings.Gift_Options_Gift_Filter_Resale )) } diff --git a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift index cdc4322ae2..d4c2e9837a 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftSetupScreen/Sources/GiftSetupScreen.swift @@ -82,6 +82,7 @@ final class GiftSetupScreenComponent: Component { private let navigationTitle = ComponentView() private let remainingCount = ComponentView() + private let resaleSection = ComponentView() private let introContent = ComponentView() private let introSection = ComponentView() private let starsSection = ComponentView() @@ -787,6 +788,59 @@ final class GiftSetupScreenComponent: Component { contentHeight += sectionSpacing } + if case let .starGift(starGift, _) = component.subject, let availability = starGift.availability, availability.resale > 0 { + let resaleSectionSize = self.resaleSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [ + AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent( + MultilineTextComponent( + text: .plain(NSAttributedString(string: "Available for Resale", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), textColor: environment.theme.list.itemPrimaryTextColor)) + ) + )), + ], alignment: .left, spacing: 2.0)), + accessory: .custom(ListActionItemComponent.CustomAccessory(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: presentationStringsFormattedNumber(Int32(availability.resale), environment.dateTimeFormat.groupingSeparator), + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 0 + ))), insets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 16.0))), + action: { [weak self] _ in + guard let self, let component = self.component, let controller = environment.controller() else { + return + } + let storeController = component.context.sharedContext.makeGiftStoreController( + context: component.context, + peerId: component.peerId, + gift: starGift + ) + controller.push(storeController) + } + ))) + ] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let resaleSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: resaleSectionSize) + if let resaleSectionView = self.resaleSection.view { + if resaleSectionView.superview == nil { + self.scrollView.addSubview(resaleSectionView) + } + transition.setFrame(view: resaleSectionView, frame: resaleSectionFrame) + } + contentHeight += resaleSectionSize.height + contentHeight += sectionSpacing + } + let giftConfiguration = GiftConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 }) var introSectionItems: [AnyComponentWithIdentity] = [] diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 6023e86c6f..0b2912e02c 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -139,21 +139,10 @@ final class GiftStoreScreenComponent: Component { self.updateScrolling(interactive: true, transition: self.nextScrollTransition ?? .immediate) } - private var removedStarGifts = Set() private var currentGifts: ([StarGift], Set, Set, Set)? private var effectiveGifts: [StarGift]? { if let gifts = self.state?.starGiftsState?.gifts { - if !self.removedStarGifts.isEmpty { - return gifts.filter { gift in - if case let .unique(uniqueGift) = gift { - return !self.removedStarGifts.contains(uniqueGift.slug) - } else { - return true - } - } - } else { - return gifts - } + return gifts } else { return nil } @@ -253,15 +242,14 @@ final class GiftStoreScreenComponent: Component { } let giftController = GiftViewScreen( context: component.context, - subject: .uniqueGift(uniqueGift, state.peerId) - ) - giftController.onBuySuccess = { [weak self] in - guard let self else { - return + subject: .uniqueGift(uniqueGift, state.peerId), + buyGift: { slug, peerId in + return self.state?.starGiftsContext.buyStarGift(slug: slug, peerId: peerId) ?? .complete() + }, + updateResellStars: { price in + return self.state?.starGiftsContext.updateStarGiftResellPrice(slug: uniqueGift.slug, price: price) ?? .complete() } - self.removedStarGifts.insert(uniqueGift.slug) - self.state?.updated(transition: .spring(duration: 0.3)) - } + ) mainController.push(giftController) } } @@ -507,15 +495,17 @@ final class GiftStoreScreenComponent: Component { //TODO:localize var items: [ContextMenuItem] = [] - items.append(.custom(SearchContextItem( - context: component.context, - placeholder: "Search", - value: "", - valueChanged: { value in - searchQueryPromise.set(value) - } - ), false)) - items.append(.separator) + if modelAttributes.count >= 8 { + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + } items.append(.custom(GiftAttributeListContextItem( context: component.context, attributes: modelAttributes, @@ -597,15 +587,17 @@ final class GiftStoreScreenComponent: Component { //TODO:localize var items: [ContextMenuItem] = [] - items.append(.custom(SearchContextItem( - context: component.context, - placeholder: "Search", - value: "", - valueChanged: { value in - searchQueryPromise.set(value) - } - ), false)) - items.append(.separator) + if backdropAttributes.count >= 8 { + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + } items.append(.custom(GiftAttributeListContextItem( context: component.context, attributes: backdropAttributes, @@ -687,15 +679,17 @@ final class GiftStoreScreenComponent: Component { //TODO:localize var items: [ContextMenuItem] = [] - items.append(.custom(SearchContextItem( - context: component.context, - placeholder: "Search", - value: "", - valueChanged: { value in - searchQueryPromise.set(value) - } - ), false)) - items.append(.separator) + if patternAttributes.count >= 8 { + items.append(.custom(SearchContextItem( + context: component.context, + placeholder: "Search", + value: "", + valueChanged: { value in + searchQueryPromise.set(value) + } + ), false)) + items.append(.separator) + } items.append(.custom(GiftAttributeListContextItem( context: component.context, attributes: patternAttributes, diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 76bf4b098c..802039c050 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -460,8 +460,6 @@ private final class GiftViewSheetContent: CombinedComponent { guard let self, let controller = self.getController() as? GiftViewScreen else { return } - controller.onBuySuccess() - self.inProgress = false var animationFile: TelegramMediaFile? @@ -2902,7 +2900,6 @@ public class GiftViewScreen: ViewControllerComponentContainer { let updateSubject = ActionSlot() public var disposed: () -> Void = {} - public var onBuySuccess: () -> Void = {} fileprivate var showBalance = false { didSet { @@ -2922,7 +2919,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { transferGift: ((Bool, EnginePeer.Id) -> Signal)? = nil, upgradeGift: ((Int64?, Bool) -> Signal)? = nil, buyGift: ((String, EnginePeer.Id) -> Signal)? = nil, - updateResellStars: ((Int64?) -> Void)? = nil, + updateResellStars: ((Int64?) -> Signal)? = nil, togglePinnedToTop: ((Bool) -> Bool)? = nil, shareStory: ((StarGift.UniqueGift) -> Void)? = nil ) { @@ -3413,6 +3410,7 @@ public class GiftViewScreen: ViewControllerComponentContainer { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let giftTitle = "\(gift.title) #\(presentationStringsFormattedNumber(gift.number, presentationData.dateTimeFormat.groupingSeparator))" + let reference = arguments.reference ?? .slug(slug: gift.slug) //TODO:localize if let resellStars = gift.resellStars, resellStars > 0, !update { @@ -3425,44 +3423,39 @@ public class GiftViewScreen: ViewControllerComponentContainer { guard let self else { return } - - switch self.subject { - case let .profileGift(peerId, currentSubject): - self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil)))) - case let .uniqueGift(_, recipientPeerId): - self.subject = .uniqueGift(gift.withResellStars(nil), recipientPeerId) - default: - break - } - self.onBuySuccess() - - let text = "\(giftTitle) is removed from sale." - let tooltipController = UndoOverlayController( - presentationData: presentationData, - content: .universalImage( - image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!, - size: nil, - title: nil, - text: text, - customUndoText: nil, - timeout: 3.0 - ), - position: .bottom, - animateInAsReplacement: false, - appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), - action: { action in - return false + let _ = ((updateResellStars?(nil) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil)) + |> deliverOnMainQueue).startStandalone(error: { error in + + }, completed: { + switch self.subject { + case let .profileGift(peerId, currentSubject): + self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(nil)))) + case let .uniqueGift(_, recipientPeerId): + self.subject = .uniqueGift(gift.withResellStars(nil), recipientPeerId) + default: + break } - ) - self.present(tooltipController, in: .window(.root)) - - if let updateResellStars { - updateResellStars(nil) - } else { - let reference = arguments.reference ?? .slug(slug: gift.slug) - let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: nil) - |> deliverOnMainQueue).startStandalone() - } + + let text = "\(giftTitle) is removed from sale." + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Unlist"), color: .white)!, + size: nil, + title: nil, + text: text, + customUndoText: nil, + timeout: 3.0 + ), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { action in + return false + } + ) + self.present(tooltipController, in: .window(.root)) + }) }), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) @@ -3476,46 +3469,47 @@ public class GiftViewScreen: ViewControllerComponentContainer { return } - switch self.subject { - case let .profileGift(peerId, currentSubject): - self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price)))) - case let .uniqueGift(_, recipientPeerId): - self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId) - default: - break - } - - var text = "\(giftTitle) is now for sale!" - if update { - text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars." - } - - let tooltipController = UndoOverlayController( - presentationData: presentationData, - content: .universalImage( - image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!, - size: nil, - title: nil, - text: text, - customUndoText: nil, - timeout: 3.0 - ), - position: .bottom, - animateInAsReplacement: false, - appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), - action: { action in - return false + let _ = ((updateResellStars?(price) ?? context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price)) + |> deliverOnMainQueue).startStandalone(error: { error in + + }, completed: { [weak self] in + guard let self else { + return } - ) - self.present(tooltipController, in: .window(.root)) - - if let updateResellStars { - updateResellStars(price) - } else { - let reference = arguments.reference ?? .slug(slug: gift.slug) - let _ = (context.engine.payments.updateStarGiftResalePrice(reference: reference, price: price) - |> deliverOnMainQueue).startStandalone() - } + + switch self.subject { + case let .profileGift(peerId, currentSubject): + self.subject = .profileGift(peerId, currentSubject.withGift(.unique(gift.withResellStars(price)))) + case let .uniqueGift(_, recipientPeerId): + self.subject = .uniqueGift(gift.withResellStars(price), recipientPeerId) + default: + break + } + + var text = "\(giftTitle) is now for sale!" + if update { + text = "\(giftTitle) is relisted for \(presentationStringsFormattedNumber(Int32(price), presentationData.dateTimeFormat.groupingSeparator)) Stars." + } + + let tooltipController = UndoOverlayController( + presentationData: presentationData, + content: .universalImage( + image: generateTintedImage(image: UIImage(bundleImageName: "Premium/Collectible/Sell"), color: .white)!, + size: nil, + title: nil, + text: text, + customUndoText: nil, + timeout: 3.0 + ), + position: .bottom, + animateInAsReplacement: false, + appearance: UndoOverlayController.Appearance(sideInset: 16.0, bottomInset: 62.0), + action: { action in + return false + } + ) + self.present(tooltipController, in: .window(.root)) + }) }) self.push(resellController) } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 90f3536f54..95104c8142 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -352,6 +352,8 @@ public final class MediaEditor { return state.position } } + + public var maxDuration: Double = 60.0 public var duration: Double? { if let stickerEntity = self.stickerEntity { @@ -360,7 +362,7 @@ public final class MediaEditor { if let trimRange = self.values.videoTrimRange { return trimRange.upperBound - trimRange.lowerBound } else { - return min(60.0, self.playerPlaybackState.duration) + return min(self.maxDuration, self.playerPlaybackState.duration) } } else { return nil @@ -369,7 +371,7 @@ public final class MediaEditor { public var mainVideoDuration: Double? { if self.player != nil { - return min(60.0, self.playerPlaybackState.duration) + return min(self.maxDuration, self.playerPlaybackState.duration) } else { return nil } @@ -377,7 +379,7 @@ public final class MediaEditor { public var additionalVideoDuration: Double? { if let additionalPlayer = self.additionalPlayers.first { - return min(60.0, additionalPlayer.currentItem?.asset.duration.seconds ?? 0.0) + return min(self.maxDuration, additionalPlayer.currentItem?.asset.duration.seconds ?? 0.0) } else { return nil } @@ -385,7 +387,15 @@ public final class MediaEditor { public var originalDuration: Double? { if self.player != nil || !self.additionalPlayers.isEmpty { - return min(60.0, self.playerPlaybackState.duration) + return self.playerPlaybackState.duration + } else { + return nil + } + } + + public var originalCappedDuration: Double? { + if self.player != nil || !self.additionalPlayers.isEmpty { + return min(self.maxDuration, self.playerPlaybackState.duration) } else { return nil } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index d0b1e891aa..47b4d4c792 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -909,7 +909,7 @@ public final class MediaEditorValues: Codable, Equatable { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } - func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { + public func withUpdatedVideoTrimRange(_ videoTrimRange: Range) -> MediaEditorValues { return MediaEditorValues(peerId: self.peerId, originalDimensions: self.originalDimensions, cropOffset: self.cropOffset, cropRect: self.cropRect, cropScale: self.cropScale, cropRotation: self.cropRotation, cropMirroring: self.cropMirroring, cropOrientation: self.cropOrientation, gradientColors: self.gradientColors, videoTrimRange: videoTrimRange, videoIsMuted: self.videoIsMuted, videoIsFullHd: self.videoIsFullHd, videoIsMirrored: self.videoIsMirrored, videoVolume: self.videoVolume, additionalVideoPath: self.additionalVideoPath, additionalVideoIsDual: self.additionalVideoIsDual, additionalVideoPosition: self.additionalVideoPosition, additionalVideoScale: self.additionalVideoScale, additionalVideoRotation: self.additionalVideoRotation, additionalVideoPositionChanges: self.additionalVideoPositionChanges, additionalVideoTrimRange: self.additionalVideoTrimRange, additionalVideoOffset: self.additionalVideoOffset, additionalVideoVolume: self.additionalVideoVolume, collage: self.collage, nightTheme: self.nightTheme, drawing: self.drawing, maskDrawing: self.maskDrawing, entities: self.entities, toolValues: self.toolValues, audioTrack: self.audioTrack, audioTrackTrimRange: self.audioTrackTrimRange, audioTrackOffset: self.audioTrackOffset, audioTrackVolume: self.audioTrackVolume, audioTrackSamples: self.audioTrackSamples, collageTrackSamples: self.collageTrackSamples, coverImageTimestamp: self.coverImageTimestamp, coverDimensions: self.coverDimensions, qualityPreset: self.qualityPreset) } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 4edda83f7c..7e4eac1d55 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -327,7 +327,7 @@ final class MediaEditorScreenComponent: Component { private let switchCameraButton = ComponentView() private let selectionButton = ComponentView() - private let selectionPanel = ComponentView() + private var selectionPanel: ComponentView? private let textCancelButton = ComponentView() private let textDoneButton = ComponentView() @@ -577,6 +577,11 @@ final class MediaEditorScreenComponent: Component { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) } + + if let view = self.selectionButton.view { + view.layer.animateAlpha(from: 0.0, to: view.alpha, duration: 0.2) + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + } } } @@ -589,14 +594,14 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: view, scale: 0.1) } - let buttons = [ + let toolbarButtons = [ self.drawButton, self.textButton, self.stickerButton, self.toolsButton ] - for button in buttons { + for button in toolbarButtons { if let view = button.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false) @@ -617,19 +622,17 @@ final class MediaEditorScreenComponent: Component { } } - if let view = self.saveButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } + let topButtons = [ + self.saveButton, + self.muteButton, + self.playbackButton + ] - if let view = self.muteButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.playbackButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + for button in topButtons { + if let view = button.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } if let view = self.scrubber?.view { @@ -638,35 +641,30 @@ final class MediaEditorScreenComponent: Component { view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } - if let view = self.undoButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } + let stickerButtons = [ + self.undoButton, + self.eraseButton, + self.restoreButton, + self.outlineButton, + self.cutoutButton + ] - if let view = self.eraseButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.restoreButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.outlineButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.cutoutButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + for button in stickerButtons { + if let view = button.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } if let view = self.textSize.view { transition.setAlpha(view: view, alpha: 0.0) transition.setScale(view: view, scale: 0.1) } + + if let view = self.selectionButton.view { + transition.setAlpha(view: view, alpha: 0.0) + transition.setScale(view: view, scale: 0.1) + } } func animateOutToTool(inPlace: Bool, transition: ComponentTransition) { @@ -2000,135 +1998,6 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: switchCameraButtonView, scale: isRecordingAdditionalVideo ? 1.0 : 0.01) transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0) } - - if controller.node.items.count > 1 { - let selectionButtonSize = self.selectionButton.update( - transition: transition, - component: AnyComponent(PlainButtonComponent( - content: AnyComponent( - SelectionPanelButtonContentComponent( - count: Int32(controller.node.items.count(where: { $0.isEnabled })), - isSelected: self.isSelectionPanelOpen, - tag: nil - ) - ), - effectAlignment: .center, - action: { [weak self, weak controller] in - if let self, let controller { - self.isSelectionPanelOpen = !self.isSelectionPanelOpen - if let mediaEditor = controller.node.mediaEditor { - if self.isSelectionPanelOpen { - mediaEditor.maybePauseVideo() - } else { - Queue.mainQueue().after(0.1) { - mediaEditor.maybeUnpauseVideo() - } - } - } - self.state?.updated() - - controller.hapticFeedback.impact(.light) - } - }, - animateAlpha: false - )), - environment: {}, - containerSize: CGSize(width: 33.0, height: 33.0) - ) - let selectionButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0), - size: selectionButtonSize - ) - if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View { - if selectionButtonView.superview == nil { - self.addSubview(selectionButtonView) - } - transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) - transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) - transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01) - transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0) - - if self.isSelectionPanelOpen { - let selectionPanelFrame = CGRect( - origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0), - size: CGSize(width: availableSize.width - 24.0, height: 120.0) - ) - - var selectedItemId = "" - if case let .asset(asset) = controller.node.subject { - selectedItemId = asset.localIdentifier - } - - let _ = self.selectionPanel.update( - transition: transition, - component: AnyComponent( - SelectionPanelComponent( - previewContainerView: controller.node.previewContentContainerView, - frame: selectionPanelFrame, - items: controller.node.items, - selectedItemId: selectedItemId, - itemTapped: { [weak self, weak controller] id in - guard let self, let controller else { - return - } - self.isSelectionPanelOpen = false - self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate) - - if let id { - controller.node.switchToItem(id) - - controller.hapticFeedback.impact(.light) - } - }, - itemSelectionToggled: { [weak self, weak controller] id in - guard let self, let controller else { - return - } - if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) { - controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled - } - self.state?.updated(transition: .spring(duration: 0.3)) - }, - itemReordered: { [weak self, weak controller] fromId, toId in - guard let self, let controller else { - return - } - guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else { - return - } - let fromItem = controller.node.items[fromIndex] - let toItem = controller.node.items[toIndex] - controller.node.items[fromIndex] = toItem - controller.node.items[toIndex] = fromItem - self.state?.updated(transition: .spring(duration: 0.3)) - - controller.hapticFeedback.tap() - } - ) - ), - environment: {}, - containerSize: availableSize - ) - if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { - if selectionPanelView.superview == nil { - self.insertSubview(selectionPanelView, belowSubview: selectionButtonView) - if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { - selectionPanelView.animateIn(from: buttonView) - } - } - selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) - } - } else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View { - if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { - selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in - selectionPanelView?.removeFromSuperview() - }) - } else { - selectionPanelView.removeFromSuperview() - } - } - } - } } else { inputPanelSize = CGSize(width: 0.0, height: 12.0) } @@ -2136,20 +2005,24 @@ final class MediaEditorScreenComponent: Component { if case .stickerEditor = controller.mode { } else { + var selectionButtonInset: CGFloat = 0.0 + if let playerState = state.playerState { let scrubberInset: CGFloat = 9.0 let minDuration: Double let maxDuration: Double + var segmentDuration: Double? if playerState.isAudioOnly { minDuration = 5.0 maxDuration = 15.0 } else { minDuration = 1.0 if case .avatarEditor = controller.mode { - maxDuration = 10.0 + maxDuration = 9.9 } else { - maxDuration = storyMaxVideoDuration + maxDuration = storyMaxCombinedVideoDuration + segmentDuration = storyMaxVideoDuration } } @@ -2224,6 +2097,7 @@ final class MediaEditorScreenComponent: Component { position: playerState.position, minDuration: minDuration, maxDuration: maxDuration, + segmentDuration: segmentDuration, isPlaying: playerState.isPlaying, tracks: visibleTracks, isCollage: isCollage, @@ -2363,6 +2237,7 @@ final class MediaEditorScreenComponent: Component { } let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - scrubberSize.height + controlsBottomInset - inputPanelSize.height + 3.0 - scrubberBottomOffset), size: scrubberSize) + selectionButtonInset = scrubberSize.height + 11.0 if let scrubberView = scrubber.view { var animateIn = false if scrubberView.superview == nil { @@ -2407,6 +2282,146 @@ final class MediaEditorScreenComponent: Component { } } } + + if controller.node.items.count > 1 { + let selectionButtonSize = self.selectionButton.update( + transition: transition, + component: AnyComponent(PlainButtonComponent( + content: AnyComponent( + SelectionPanelButtonContentComponent( + count: Int32(controller.node.items.count(where: { $0.isEnabled })), + isSelected: self.isSelectionPanelOpen, + tag: nil + ) + ), + effectAlignment: .center, + action: { [weak self, weak controller] in + if let self, let controller { + self.isSelectionPanelOpen = !self.isSelectionPanelOpen + if let mediaEditor = controller.node.mediaEditor { + if self.isSelectionPanelOpen { + mediaEditor.maybePauseVideo() + } else { + Queue.mainQueue().after(0.1) { + mediaEditor.maybeUnpauseVideo() + } + } + } + self.state?.updated(transition: .spring(duration: 0.3)) + + controller.hapticFeedback.impact(.light) + } + }, + animateAlpha: false + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + let selectionButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: availableSize.height - environment.safeInsets.bottom - selectionButtonSize.height + controlsBottomInset - inputPanelSize.height - 3.0 - selectionButtonInset), + size: selectionButtonSize + ) + if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View { + if selectionButtonView.superview == nil { + self.addSubview(selectionButtonView) + } + transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center) + transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size)) + transition.setScale(view: selectionButtonView, scale: displayTopButtons && !isRecordingAdditionalVideo ? 1.0 : 0.01) + transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities && !isRecordingAdditionalVideo ? 1.0 : 0.0) + + if self.isSelectionPanelOpen { + let selectionPanelFrame = CGRect( + origin: CGPoint(x: 12.0, y: selectionButtonFrame.minY - 130.0), + size: CGSize(width: availableSize.width - 24.0, height: 120.0) + ) + + var selectedItemId = "" + if case let .asset(asset) = controller.node.subject { + selectedItemId = asset.localIdentifier + } + + let selectionPanel: ComponentView + if let current = self.selectionPanel { + selectionPanel = current + } else { + selectionPanel = ComponentView() + self.selectionPanel = selectionPanel + } + + let _ = selectionPanel.update( + transition: transition, + component: AnyComponent( + SelectionPanelComponent( + previewContainerView: controller.node.previewContentContainerView, + frame: selectionPanelFrame, + items: controller.node.items, + selectedItemId: selectedItemId, + itemTapped: { [weak self, weak controller] id in + guard let self, let controller else { + return + } + self.isSelectionPanelOpen = false + self.state?.updated(transition: id == nil ? .spring(duration: 0.3) : .immediate) + + if let id { + controller.node.switchToItem(id) + + controller.hapticFeedback.impact(.light) + } + }, + itemSelectionToggled: { [weak self, weak controller] id in + guard let self, let controller else { + return + } + if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) { + controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled + } + self.state?.updated(transition: .spring(duration: 0.3)) + }, + itemReordered: { [weak self, weak controller] fromId, toId in + guard let self, let controller else { + return + } + guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else { + return + } + let fromItem = controller.node.items[fromIndex] + let toItem = controller.node.items[toIndex] + controller.node.items[fromIndex] = toItem + controller.node.items[toIndex] = fromItem + self.state?.updated(transition: .spring(duration: 0.3)) + + controller.hapticFeedback.tap() + } + ) + ), + environment: {}, + containerSize: availableSize + ) + if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View { + if selectionPanelView.superview == nil { + self.insertSubview(selectionPanelView, belowSubview: selectionButtonView) + if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateIn(from: buttonView) + } + } + selectionPanelView.frame = CGRect(origin: .zero, size: availableSize) + } + } else if let selectionPanel = self.selectionPanel { + self.selectionPanel = nil + if let selectionPanelView = selectionPanel.view as? SelectionPanelComponent.View { + if !transition.animation.isImmediate, let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View { + selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in + selectionPanelView?.removeFromSuperview() + }) + } else { + selectionPanelView.removeFromSuperview() + } + } + } + } + } } if case .stickerEditor = controller.mode { @@ -2821,6 +2836,8 @@ final class MediaEditorScreenComponent: Component { let storyDimensions = CGSize(width: 1080.0, height: 1920.0) let storyMaxVideoDuration: Double = 60.0 +let storyMaxCombinedVideoCount: Int = 3 +let storyMaxCombinedVideoDuration: Double = storyMaxVideoDuration * Double(storyMaxCombinedVideoCount) public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UIDropInteractionDelegate { public enum Mode { @@ -3489,6 +3506,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID values: initialValues, hasHistogram: true ) + mediaEditor.maxDuration = storyMaxCombinedVideoDuration if case .avatarEditor = controller.mode { mediaEditor.setVideoIsMuted(true) } else if case let .coverEditor(dimensions) = controller.mode { @@ -5075,7 +5093,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID var audioTrimRange: Range? var audioOffset: Double? - if let videoDuration = mediaEditor.originalDuration { + if let videoDuration = mediaEditor.originalCappedDuration { if let videoStart = mediaEditor.values.videoTrimRange?.lowerBound { audioOffset = -videoStart } else if let _ = mediaEditor.values.additionalVideoPath, let videoStart = mediaEditor.values.additionalVideoTrimRange?.lowerBound { @@ -6694,7 +6712,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } self.postingAvailabilityDisposable = (self.postingAvailabilityPromise.get() |> deliverOnMainQueue).start(next: { [weak self] availability in - guard let self, availability != .available else { + guard let self else { + return + } + if case .available = availability { return } @@ -7341,36 +7362,21 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return true } - private func completeWithMultipleResults(results: [MediaEditorScreenImpl.Result]) { - // Send all results to completion handler - self.completion(results, { [weak self] finished in - self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in - self?.dismiss() - Queue.mainQueue().justDispatch { - finished() - } - }) - }) - } - - private func processMultipleItems() { - guard !self.node.items.isEmpty else { + private func processMultipleItems(items: [EditingItem]) { + guard !items.isEmpty else { return } - if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = self.node.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) { - let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } - let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) - mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - - var updatedCurrentItem = self.node.items[currentItemIndex] + var items = items + if let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) { + var updatedCurrentItem = items[currentItemIndex] updatedCurrentItem.caption = self.node.getCaption() updatedCurrentItem.values = mediaEditor.values - self.node.items[currentItemIndex] = updatedCurrentItem + items[currentItemIndex] = updatedCurrentItem } let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: []) - let totalItems = self.node.items.count + let totalItems = items.count let dispatchGroup = DispatchGroup() @@ -7387,7 +7393,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID } var order: [Int64] = [] - for (index, item) in self.node.items.enumerated() { + for (index, item) in items.enumerated() { guard item.isEnabled else { continue } @@ -7431,7 +7437,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID orderedResults.append(item) } } - self.completeWithMultipleResults(results: orderedResults) + self.completion(results, { [weak self] finished in + self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) } } } @@ -7452,13 +7465,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if let mediaArea = entity.mediaArea { mediaAreas.append(mediaArea) } - - // Extract stickers from entities extractStickersFromEntity(entity, into: &stickers) } } - // Process video let firstFrameTime: CMTime if let coverImageTimestamp = item.values?.coverImageTimestamp { firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60)) @@ -7476,7 +7486,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } - // Calculate duration let duration: Double if let videoTrimRange = item.values?.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound @@ -7484,7 +7493,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID duration = min(asset.duration, storyMaxVideoDuration) } - // Generate thumbnail frame let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) avAssetGenerator.appliesPreferredTrackTransform = true avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in @@ -7541,14 +7549,11 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) { let asset = item.asset - // Setup temporary media editor for this item let itemMediaEditor = setupMediaEditorForItem(item: item) - // Get caption for this item var caption = item.caption caption = convertMarkdownToAttributes(caption) - // Media areas and stickers var mediaAreas: [MediaArea] = [] var stickers: [TelegramMediaFile] = [] @@ -7557,13 +7562,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if let mediaArea = entity.mediaArea { mediaAreas.append(mediaArea) } - - // Extract stickers from entities extractStickersFromEntity(entity, into: &stickers) } } - // Request full-size image let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.isNetworkAccessAllowed = true @@ -7664,10 +7666,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else { return } - - let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } - let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) - mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) var caption = self.node.getCaption() caption = convertMarkdownToAttributes(caption) @@ -7680,6 +7678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID randomId = Int64.random(in: .min ... .max) } + let codableEntities = mediaEditor.values.entities var mediaAreas: [MediaArea] = [] if case let .draft(draft, _) = actualSubject { if draft.values.entities != codableEntities { @@ -8108,6 +8107,15 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }) } } + + private func updateMediaEditorEntities() { + guard let mediaEditor = self.node.mediaEditor else { + return + } + let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } + let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) + mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) + } private var didComplete = false func requestStoryCompletion(animated: Bool) { @@ -8117,7 +8125,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID self.didComplete = true - self.dismissAllTooltips() + self.updateMediaEditorEntities() mediaEditor.stop() mediaEditor.invalidate() @@ -8127,11 +8135,42 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - if self.node.items.count(where: { $0.isEnabled }) > 1 { - self.processMultipleItems() + var multipleItems: [EditingItem] = [] + if self.node.items.count > 1 { + multipleItems = self.node.items.filter({ $0.isEnabled }) + } else if case let .asset(asset) = self.node.subject { + let duration: Double + if let playerDuration = mediaEditor.duration { + duration = playerDuration + } else { + duration = asset.duration + } + if duration > storyMaxVideoDuration { + let originalDuration = mediaEditor.originalDuration ?? asset.duration + let values = mediaEditor.values + + let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration))) + var start = values.videoTrimRange?.lowerBound ?? 0 + for _ in 0 ..< storyCount { + let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(start + storyMaxVideoDuration, originalDuration)) + + var editingItem = EditingItem(asset: asset) + editingItem.caption = self.node.getCaption() + editingItem.values = trimmedValues + multipleItems.append(editingItem) + + start += storyMaxVideoDuration + } + } + } + + if multipleItems.count > 1 { + self.processMultipleItems(items: multipleItems) } else { self.processSingleItem() } + + self.dismissAllTooltips() } func requestStickerCompletion(animated: Bool) { @@ -8157,10 +8196,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } - let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) - mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - + self.updateMediaEditorEntities() + if let image = mediaEditor.resultImage { let values = mediaEditor.values.withUpdatedQualityPreset(.sticker) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: CGSize(width: 512, height: 512), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in @@ -8181,11 +8218,9 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID if let navigationController = self.navigationController as? NavigationController { navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) } - - let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } - let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) - mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - + + self.updateMediaEditorEntities() + if let image = mediaEditor.resultImage { let values = mediaEditor.values.withUpdatedCoverDimensions(dimensions) makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: image, dimensions: storyDimensions, outputDimensions: dimensions.aspectFitted(CGSize(width: 1080, height: 1080)), values: values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in @@ -8786,12 +8821,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID return } - let context = self.context + self.updateMediaEditorEntities() - let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } - let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) - mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - let isSticker = toStickerResource != nil if !isSticker { self.previousSavedValues = mediaEditor.values @@ -8820,6 +8851,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID }) } + let context = self.context if mediaEditor.resultIsVideo { if !isSticker { mediaEditor.maybePauseVideo() diff --git a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift index 3a5b90a3b3..f7847fee37 100644 --- a/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift +++ b/submodules/TelegramUI/Components/MediaScrubberComponent/Sources/MediaScrubberComponent.swift @@ -84,6 +84,7 @@ public final class MediaScrubberComponent: Component { let position: Double let minDuration: Double let maxDuration: Double + let segmentDuration: Double? let isPlaying: Bool let tracks: [Track] @@ -112,6 +113,7 @@ public final class MediaScrubberComponent: Component { position: Double, minDuration: Double, maxDuration: Double, + segmentDuration: Double? = nil, isPlaying: Bool, tracks: [Track], isCollage: Bool, @@ -135,6 +137,7 @@ public final class MediaScrubberComponent: Component { self.position = position self.minDuration = minDuration self.maxDuration = maxDuration + self.segmentDuration = segmentDuration self.isPlaying = isPlaying self.tracks = tracks self.isCollage = isCollage @@ -171,6 +174,9 @@ public final class MediaScrubberComponent: Component { if lhs.maxDuration != rhs.maxDuration { return false } + if lhs.segmentDuration != rhs.segmentDuration { + return false + } if lhs.isPlaying != rhs.isPlaying { return false } @@ -624,6 +630,7 @@ public final class MediaScrubberComponent: Component { isSelected: isSelected, availableSize: availableSize, duration: self.duration, + segmentDuration: lowestVideoId == track.id ? component.segmentDuration : nil, transition: trackTransition ) trackLayout[id] = (CGRect(origin: CGPoint(x: 0.0, y: totalHeight), size: trackSize), trackTransition, animateTrackIn) @@ -675,6 +682,7 @@ public final class MediaScrubberComponent: Component { isSelected: false, availableSize: availableSize, duration: self.duration, + segmentDuration: nil, transition: trackTransition ) trackTransition.setFrame(view: trackView, frame: CGRect(origin: .zero, size: trackSize)) @@ -955,6 +963,9 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega fileprivate let audioContentMaskView: UIImageView fileprivate let audioIconView: UIImageView fileprivate let audioTitle = ComponentView() + + fileprivate var segmentTitles: [Int32: ComponentView] = [:] + fileprivate var segmentLayers: [Int32: SimpleLayer] = [:] fileprivate let videoTransparentFramesContainer = UIView() fileprivate var videoTransparentFrameLayers: [VideoFrameLayer] = [] @@ -1142,6 +1153,7 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega isSelected: Bool, availableSize: CGSize, duration: Double, + segmentDuration: Double?, transition: ComponentTransition ) -> CGSize { let previousParams = self.params @@ -1477,6 +1489,86 @@ private class TrackView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelega transition.setFrame(view: self.vibrancyView, frame: CGRect(origin: .zero, size: containerFrame.size)) transition.setFrame(view: self.vibrancyContainer, frame: CGRect(origin: .zero, size: containerFrame.size)) + var segmentCount = 0 + var segmentOrigin: CGFloat = 0.0 + var segmentWidth: CGFloat = 0.0 + if let segmentDuration { + if duration > segmentDuration { + let fraction = segmentDuration / duration + segmentCount = Int(ceil(duration / segmentDuration)) - 1 + segmentWidth = floorToScreenPixels(containerFrame.width * fraction) + } + if let trimRange = track.trimRange { + if trimRange.lowerBound > 0.0 { + let fraction = trimRange.lowerBound / duration + segmentOrigin = floorToScreenPixels(containerFrame.width * fraction) + } + let actualSegmentCount = Int(ceil((trimRange.upperBound - trimRange.lowerBound) / segmentDuration)) - 1 + segmentCount = min(actualSegmentCount, segmentCount) + } + } + + var validIds = Set() + var segmentFrame = CGRect(x: segmentOrigin + segmentWidth, y: 0.0, width: 1.0, height: containerFrame.size.height) + for i in 0 ..< segmentCount { + let id = Int32(i) + validIds.insert(id) + + let segmentLayer: SimpleLayer + let segmentTitle: ComponentView + + var segmentTransition = transition + if let currentLayer = self.segmentLayers[id], let currentTitle = self.segmentTitles[id] { + segmentLayer = currentLayer + segmentTitle = currentTitle + } else { + segmentTransition = .immediate + segmentLayer = SimpleLayer() + segmentLayer.backgroundColor = UIColor.white.cgColor + segmentTitle = ComponentView() + + self.segmentLayers[id] = segmentLayer + self.segmentTitles[id] = segmentTitle + + self.containerView.layer.addSublayer(segmentLayer) + } + + transition.setFrame(layer: segmentLayer, frame: segmentFrame) + + let segmentTitleSize = segmentTitle.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "#\(i + 2)", font: Font.semibold(11.0), textColor: .white)), + textShadowColor: UIColor(rgb: 0x000000, alpha: 0.4), + textShadowBlur: 1.0 + )), + environment: {}, + containerSize: containerFrame.size + ) + if let view = segmentTitle.view { + if view.superview == nil { + self.containerView.addSubview(view) + } + segmentTransition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: segmentFrame.maxX + 2.0, y: 2.0), size: segmentTitleSize)) + } + segmentFrame.origin.x += segmentWidth + } + + var removeIds: [Int32] = [] + for (id, segmentLayer) in self.segmentLayers { + if !validIds.contains(id) { + removeIds.append(id) + segmentLayer.removeFromSuperlayer() + if let segmentTitle = self.segmentTitles[id] { + segmentTitle.view?.removeFromSuperview() + } + } + } + for id in removeIds { + self.segmentLayers.removeValue(forKey: id) + self.segmentTitles.removeValue(forKey: id) + } + return scrubberSize } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index b3a328a3b1..fc4b92105e 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -613,6 +613,8 @@ private final class PeerInfoInteraction { let openBirthdayContextMenu: (ASDisplayNode, ContextGesture?) -> Void let editingOpenAffiliateProgram: () -> Void let editingOpenVerifyAccounts: () -> Void + let editingToggleAutoTranslate: (Bool) -> Void + let displayAutoTranslateLocked: () -> Void let getController: () -> ViewController? init( @@ -683,6 +685,8 @@ private final class PeerInfoInteraction { openBirthdayContextMenu: @escaping (ASDisplayNode, ContextGesture?) -> Void, editingOpenAffiliateProgram: @escaping () -> Void, editingOpenVerifyAccounts: @escaping () -> Void, + editingToggleAutoTranslate: @escaping (Bool) -> Void, + displayAutoTranslateLocked: @escaping () -> Void, getController: @escaping () -> ViewController? ) { self.openUsername = openUsername @@ -752,6 +756,8 @@ private final class PeerInfoInteraction { self.openBirthdayContextMenu = openBirthdayContextMenu self.editingOpenAffiliateProgram = editingOpenAffiliateProgram self.editingOpenVerifyAccounts = editingOpenVerifyAccounts + self.editingToggleAutoTranslate = editingToggleAutoTranslate + self.displayAutoTranslateLocked = displayAutoTranslateLocked self.getController = getController } } @@ -2154,6 +2160,7 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL let ItemBanned = 11 let ItemRecentActions = 12 let ItemAffiliatePrograms = 13 + let ItemPeerAutoTranslate = 14 let isCreator = channel.flags.contains(.isCreator) @@ -2268,6 +2275,18 @@ private func editingItems(data: PeerInfoScreenData?, state: PeerInfoState, chatL items[.peerSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerColor, label: .image(colorImage, colorImage.size), additionalBadgeIcon: boostIcon, text: presentationData.strings.Channel_Info_AppearanceItem, icon: UIImage(bundleImageName: "Chat/Info/NameColorIcon"), action: { interaction.editingOpenNameColorSetup() })) + + var isLocked = true + if let approximateBoostLevel = channel.approximateBoostLevel, approximateBoostLevel >= 3 { + isLocked = false + } + items[.peerSettings]!.append(PeerInfoScreenSwitchItem(id: ItemPeerAutoTranslate, text: presentationData.strings.Channel_Info_AutoTranslate, value: false, icon: UIImage(bundleImageName: "Settings/Menu/AutoTranslate"), isLocked: isLocked, toggled: { value in + if isLocked { + interaction.displayAutoTranslateLocked() + } else { + interaction.editingToggleAutoTranslate(value) + } + })) } var canEditMembers = false @@ -3194,6 +3213,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro return } self.editingOpenVerifyAccounts() + }, editingToggleAutoTranslate: { [weak self] isEnabled in + guard let self else { + return + } + self.toggleAutoTranslate(isEnabled: isEnabled) + }, displayAutoTranslateLocked: { [weak self] in + guard let self else { + return + } + self.displayAutoTranslateLocked() }, getController: { [weak self] in return self?.controller @@ -9127,6 +9156,28 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } } + private func toggleAutoTranslate(isEnabled: Bool) { + + } + + private func displayAutoTranslateLocked() { + let _ = combineLatest( + queue: Queue.mainQueue(), + context.engine.peers.getChannelBoostStatus(peerId: self.peerId), + context.engine.peers.getMyBoostStatus() + ).startStandalone(next: { [weak self] boostStatus, myBoostStatus in + guard let self, let controller = self.controller, let boostStatus, let myBoostStatus else { + return + } + let boostController = self.context.sharedContext.makePremiumBoostLevelsController(context: self.context, peerId: self.peerId, subject: .autoTranslate, boostStatus: boostStatus, myBoostStatus: myBoostStatus, forceDark: false, openStats: { [weak self] in + if let self { + self.openStats(section: .boosts, boostStatus: boostStatus) + } + }) + controller.push(boostController) + }) + } + private func toggleForumTopics(isEnabled: Bool) { guard let data = self.data, let peer = data.peer else { return diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift index 1b70f7765d..2c7db7f507 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift @@ -608,9 +608,9 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr }, updateResellStars: { [weak self] price in guard let self, let reference = product.reference else { - return + return .never() } - self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price) + return self.profileGifts.updateStarGiftResellPrice(reference: reference, price: price) }, togglePinnedToTop: { [weak self] pinnedToTop in guard let self else { diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift index 8b4021c46c..7858518c4d 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionScreen/Sources/StarsTransactionScreen.swift @@ -173,6 +173,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { let spaceRegex = try? NSRegularExpression(pattern: "\\[(.*?)\\]", options: []) + let giftCompositionExternalState = GiftCompositionComponent.ExternalState() + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let controller = environment.controller @@ -366,8 +368,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { } case let .transaction(transaction, parentPeer): if let starGift = transaction.starGift { - titleText = strings.Stars_Transaction_Gift_Title - descriptionText = "" + switch starGift { + case .generic: + titleText = strings.Stars_Transaction_Gift_Title + descriptionText = "" + case let .unique(gift): + titleText = gift.title + descriptionText = "\(strings.Gift_Unique_Collectible) #\(presentationStringsFormattedNumber(gift.number, dateTimeFormat.groupingSeparator))" + } count = transaction.count transactionId = transaction.id date = transaction.date @@ -665,14 +673,23 @@ private final class StarsTransactionSheetContent: CombinedComponent { } } else { amountText = "+ \(formattedAmount)" - countColor = theme.list.itemDisclosureActions.constructive.fillColor + if case .unique = giftAnimationSubject { + countColor = .white + } else { + countColor = theme.list.itemDisclosureActions.constructive.fillColor + } } - + + var titleFont = Font.bold(25.0) + if case .unique = giftAnimationSubject { + titleFont = Font.bold(20.0) + } + let title = title.update( component: MultilineTextComponent( text: .plain(NSAttributedString( string: titleText, - font: Font.bold(25.0), + font: titleFont, textColor: headerTextColor, paragraphAlignment: .center )), @@ -723,7 +740,7 @@ private final class StarsTransactionSheetContent: CombinedComponent { if let giftAnimationSubject { let animationHeight: CGFloat if case .unique = giftAnimationSubject { - animationHeight = 240.0 + animationHeight = 268.0 } else { animationHeight = 210.0 } @@ -731,7 +748,8 @@ private final class StarsTransactionSheetContent: CombinedComponent { component: GiftCompositionComponent( context: component.context, theme: theme, - subject: giftAnimationSubject + subject: giftAnimationSubject, + externalState: giftCompositionExternalState ), availableSize: CGSize(width: context.availableSize.width, height: animationHeight), transition: .immediate @@ -816,6 +834,14 @@ private final class StarsTransactionSheetContent: CombinedComponent { MultilineTextComponent(text: .plain(NSAttributedString(string: strings.Stars_Transaction_GiftUpgrade, font: tableFont, textColor: tableTextColor))) ) )) + } else if case .unique = giftAnimationSubject { + tableItems.append(.init( + id: "reason", + title: strings.Stars_Transaction_Giveaway_Reason, + component: AnyComponent( + MultilineTextComponent(text: .plain(NSAttributedString(string: count < StarsAmount.zero ? strings.Stars_Transaction_GiftPurchase : strings.Stars_Transaction_GiftSale, font: tableFont, textColor: tableTextColor))) + ) + )) } if isGift, toPeer == nil { @@ -1300,13 +1326,29 @@ private final class StarsTransactionSheetContent: CombinedComponent { ) var originY: CGFloat = 156.0 - if let _ = giftAnimationSubject { - originY += 18.0 + switch giftAnimationSubject { + case .generic: + originY += 20.0 + case .unique: + originY += 34.0 + default: + break } context.add(title .position(CGPoint(x: context.availableSize.width / 2.0, y: originY)) ) - originY += 21.0 + if case .unique = giftAnimationSubject { + originY += 17.0 + } else { + originY += 21.0 + } + + let vibrantColor: UIColor + if let previewPatternColor = giftCompositionExternalState.previewPatternColor { + vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3) + } else { + vibrantColor = UIColor.white.withAlphaComponent(0.6) + } var descriptionSize: CGSize = .zero if !descriptionText.isEmpty { @@ -1316,8 +1358,18 @@ private final class StarsTransactionSheetContent: CombinedComponent { if state.cachedChevronImage == nil || state.cachedChevronImage?.1 !== environment.theme { state.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: linkColor)!, theme) } + + var textFont = Font.regular(15.0) + let boldTextFont = Font.semibold(15.0) + var textColor = theme.actionSheet.secondaryTextColor + if case .unique = giftAnimationSubject { + textFont = Font.regular(13.0) + textColor = vibrantColor + } else if countOnTop && !isSubscriber { + textColor = theme.list.itemPrimaryTextColor + } + let linkColor = theme.actionSheet.controlAccentColor - let textColor = countOnTop && !isSubscriber ? theme.list.itemPrimaryTextColor : textColor let markdownAttributes = MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: textColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), link: MarkdownAttributeSet(font: textFont, textColor: linkColor), linkAttribute: { contents in return (TelegramTextAttributes.URL, contents) }) @@ -1362,7 +1414,13 @@ private final class StarsTransactionSheetContent: CombinedComponent { context.add(description .position(CGPoint(x: context.availableSize.width / 2.0, y: descriptionOrigin + description.size.height / 2.0)) ) - originY += description.size.height + 10.0 + originY += description.size.height + + if case .unique = giftAnimationSubject { + originY += 6.0 + } else { + originY += 10.0 + } } let amountSpacing: CGFloat = countBackgroundColor != nil ? 4.0 : 1.0 diff --git a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift index f9aacdbe72..9d29afc635 100644 --- a/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stars/StarsTransactionsScreen/Sources/StarsTransactionsListPanelComponent.swift @@ -317,9 +317,18 @@ final class StarsTransactionsListPanelComponent: Component { uniqueGift = gift } else { itemTitle = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast) - itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift - if case let .generic(gift) = starGift { + switch starGift { + case let .generic(gift): itemFile = gift.file + itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_ConvertedGift : environment.strings.Stars_Intro_Transaction_Gift + case let .unique(gift): + for attribute in gift.attributes { + if case let .model(_, file, _) = attribute { + itemFile = file + break + } + } + itemSubtitle = item.count > StarsAmount.zero ? environment.strings.Stars_Intro_Transaction_GiftSale : environment.strings.Stars_Intro_Transaction_GiftPurchase } } } else if let _ = item.giveawayMessageId { diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json new file mode 100644 index 0000000000..3bc8fbe27a --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "translation.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf new file mode 100644 index 0000000000..1715735f63 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Settings/Menu/AutoTranslate.imageset/translation.pdf differ diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index d07a2bf88d..c22df4eb3e 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -977,8 +977,8 @@ private func extractAccountManagerState(records: AccountRecordsView Void)? let sharedContext = SharedAccountContextImpl(mainWindow: self.mainWindow, sharedContainerPath: legacyBasePath, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings, networkArguments: networkArguments, hasInAppPurchases: buildConfig.isAppStoreBuild && buildConfig.apiId == 1, rootPath: rootPath, legacyBasePath: legacyBasePath, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), firebaseSecretStream: self.firebaseSecretStream.get(), setNotificationCall: { call in setPresentationCall?(call) - }, navigateToChat: { accountId, peerId, messageId in - self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil) + }, navigateToChat: { accountId, peerId, messageId, alwaysKeepMessageId in + self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: nil, messageId: messageId, storyId: nil, alwaysKeepMessageId: alwaysKeepMessageId) }, displayUpgradeProgress: { progress in if let progress = progress { if self.dataImportSplash == nil { @@ -2736,7 +2736,7 @@ private func extractAccountManagerState(records: AccountRecordsView take(1) |> deliverOnMainQueue @@ -2755,7 +2755,7 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { context in - context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny) + context.openChatWithPeerId(peerId: peerId, threadId: threadId, messageId: messageId, activateInput: activateInput, storyId: storyId, openAppIfAny: openAppIfAny, alwaysKeepMessageId: alwaysKeepMessageId) })) } diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 1f0026c3e4..f27d52dbb9 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -896,7 +896,7 @@ final class AuthorizedApplicationContext { })) } - func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false) { + func openChatWithPeerId(peerId: PeerId, threadId: Int64?, messageId: MessageId? = nil, activateInput: Bool = false, storyId: StoryId?, openAppIfAny: Bool = false, alwaysKeepMessageId: Bool = false) { if let storyId { var controllers = self.rootController.viewControllers controllers = controllers.filter { c in @@ -950,7 +950,7 @@ final class AuthorizedApplicationContext { if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController { self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil) } else { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: alwaysKeepMessageId || isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil)) } }) } diff --git a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift index 89f69e5ddb..670abb783b 100644 --- a/submodules/TelegramUI/Sources/MakeTempAccountContext.swift +++ b/submodules/TelegramUI/Sources/MakeTempAccountContext.swift @@ -39,7 +39,7 @@ public func makeTempContext( firebaseSecretStream: .never(), setNotificationCall: { _ in }, - navigateToChat: { _, _, _ in + navigateToChat: { _, _, _, _ in }, displayUpgradeProgress: { _ in }, appDelegate: nil diff --git a/submodules/TelegramUI/Sources/NotificationContentContext.swift b/submodules/TelegramUI/Sources/NotificationContentContext.swift index 973a939726..5fa8c6edf8 100644 --- a/submodules/TelegramUI/Sources/NotificationContentContext.swift +++ b/submodules/TelegramUI/Sources/NotificationContentContext.swift @@ -140,7 +140,7 @@ public final class NotificationViewControllerImpl { return nil }) - sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), externalRequestVerificationStream: .never(), externalRecaptchaRequestVerification: { _, _ in return .never() }, autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _ in }, appDelegate: nil) + sharedAccountContext = SharedAccountContextImpl(mainWindow: nil, sharedContainerPath: self.initializationData.appGroupPath, basePath: rootPath, encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: self.initializationData.encryptionParameters.0)!, salt: ValueBoxEncryptionParameters.Salt(data: self.initializationData.encryptionParameters.1)!), accountManager: accountManager, appLockContext: appLockContext, notificationController: nil, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: NetworkInitializationArguments(apiId: self.initializationData.apiId, apiHash: self.initializationData.apiHash, languagesCategory: self.initializationData.languagesCategory, appVersion: self.initializationData.appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(self.initializationData.bundleData), externalRequestVerificationStream: .never(), externalRecaptchaRequestVerification: { _, _ in return .never() }, autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider(), deviceModelName: nil, useBetaFeatures: self.initializationData.useBetaFeatures, isICloudEnabled: false), hasInAppPurchases: false, rootPath: rootPath, legacyBasePath: nil, apsNotificationToken: .never(), voipNotificationToken: .never(), firebaseSecretStream: .never(), setNotificationCall: { _ in }, navigateToChat: { _, _, _, _ in }, appDelegate: nil) presentationDataPromise.set(sharedAccountContext!.presentationData) } diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 49c0dcc2e9..474a9c21be 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -133,7 +133,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } } - private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?) -> Void + private let navigateToChatImpl: (AccountRecordId, PeerId, MessageId?, Bool) -> Void private let apsNotificationToken: Signal private let voipNotificationToken: Signal @@ -268,7 +268,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { private let energyUsageAutomaticDisposable = MetaDisposable() - init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) { + init(mainWindow: Window1?, sharedContainerPath: String, basePath: String, encryptionParameters: ValueBoxEncryptionParameters, accountManager: AccountManager, appLockContext: AppLockContext, notificationController: NotificationContainerController?, applicationBindings: TelegramApplicationBindings, initialPresentationDataAndSettings: InitialPresentationDataAndSettings, networkArguments: NetworkInitializationArguments, hasInAppPurchases: Bool, rootPath: String, legacyBasePath: String?, apsNotificationToken: Signal, voipNotificationToken: Signal, firebaseSecretStream: Signal<[String: String], NoError>, setNotificationCall: @escaping (PresentationCall?) -> Void, navigateToChat: @escaping (AccountRecordId, PeerId, MessageId?, Bool) -> Void, displayUpgradeProgress: @escaping (Float?) -> Void = { _ in }, appDelegate: AppDelegate?) { assert(Queue.mainQueue().isCurrent()) precondition(!testHasInstance) @@ -1760,7 +1760,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { } public func navigateToChat(accountId: AccountRecordId, peerId: PeerId, messageId: MessageId?) { - self.navigateToChatImpl(accountId, peerId, messageId) + self.navigateToChatImpl(accountId, peerId, messageId, true) } public func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError> {