mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Cherry-pick various fixes
This commit is contained in:
parent
96279df59b
commit
a7c63e1b55
@ -10297,8 +10297,8 @@ Sorry for the inconvenience.";
|
|||||||
"Stats.Boosts.Gift" = "Gift";
|
"Stats.Boosts.Gift" = "Gift";
|
||||||
"Stats.Boosts.TabBoosts_1" = "%@ Boost";
|
"Stats.Boosts.TabBoosts_1" = "%@ Boost";
|
||||||
"Stats.Boosts.TabBoosts_any" = "%@ Boosts";
|
"Stats.Boosts.TabBoosts_any" = "%@ Boosts";
|
||||||
"Stats.Boosts.TabGifts_1" = "%@ Boost";
|
"Stats.Boosts.TabGifts_1" = "%@ Gift";
|
||||||
"Stats.Boosts.TabGifts_any" = "%@ Boosts";
|
"Stats.Boosts.TabGifts_any" = "%@ Gifts";
|
||||||
"Stats.Boosts.ToBeDistributed" = "To Be Distributed";
|
"Stats.Boosts.ToBeDistributed" = "To Be Distributed";
|
||||||
"Stats.Boosts.Unclaimed" = "Unclaimed";
|
"Stats.Boosts.Unclaimed" = "Unclaimed";
|
||||||
"Stats.Boosts.GetBoosts" = "Get Boosts via Gifts";
|
"Stats.Boosts.GetBoosts" = "Get Boosts via Gifts";
|
||||||
@ -10364,6 +10364,8 @@ Sorry for the inconvenience.";
|
|||||||
"Chat.Giveaway.Info.DidntWin" = "You didn't win a prize in this giveaway.";
|
"Chat.Giveaway.Info.DidntWin" = "You didn't win a prize in this giveaway.";
|
||||||
"Chat.Giveaway.Info.ViewPrize" = "View My Prize";
|
"Chat.Giveaway.Info.ViewPrize" = "View My Prize";
|
||||||
|
|
||||||
|
"Chat.Giveaway.Info.FullDate" = "**%1$@** on **%2$@**";
|
||||||
|
|
||||||
"Chat.Giveaway.Toast.NotAllowed" = "You can't participate in this giveaway.";
|
"Chat.Giveaway.Toast.NotAllowed" = "You can't participate in this giveaway.";
|
||||||
"Chat.Giveaway.Toast.Participating" = "You are participating in this giveaway.";
|
"Chat.Giveaway.Toast.Participating" = "You are participating in this giveaway.";
|
||||||
"Chat.Giveaway.Toast.NotQualified" = "You are not qualified for this giveaway yet.";
|
"Chat.Giveaway.Toast.NotQualified" = "You are not qualified for this giveaway yet.";
|
||||||
@ -10414,6 +10416,7 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"ChannelBoost.EnableColors" = "Enable Colors";
|
"ChannelBoost.EnableColors" = "Enable Colors";
|
||||||
"ChannelBoost.EnableColorsText" = "Your channel needs %1$@ to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:";
|
"ChannelBoost.EnableColorsText" = "Your channel needs %1$@ to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:";
|
||||||
|
"ChannelBoost.EnableColorsLevelText" = "Your channel needs **Level %1$@** to change channel color.\n\nAsk your **Premium** subscribers to boost your channel with this link:";
|
||||||
"ChannelBoost.BoostAgain" = "Boost Again";
|
"ChannelBoost.BoostAgain" = "Boost Again";
|
||||||
|
|
||||||
"Settings.New" = "NEW";
|
"Settings.New" = "NEW";
|
||||||
@ -10423,3 +10426,10 @@ Sorry for the inconvenience.";
|
|||||||
"Channel.ChannelColor" = "Channel Color";
|
"Channel.ChannelColor" = "Channel Color";
|
||||||
|
|
||||||
"TextFormat.Code" = "Code";
|
"TextFormat.Code" = "Code";
|
||||||
|
|
||||||
|
"Notification.ChannelJoinedByYou" = "You joined the channel";
|
||||||
|
|
||||||
|
"CountriesList.SelectCountries" = "Select Countries";
|
||||||
|
"CountriesList.SaveCountries" = "Save Countries";
|
||||||
|
"CountriesList.SelectUpTo_1" = "select up to %@ country";
|
||||||
|
"CountriesList.SelectUpTo_any" = "select up to %@ countries";
|
||||||
|
@ -1064,7 +1064,7 @@ public protocol AccountContext: AnyObject {
|
|||||||
|
|
||||||
public struct PremiumConfiguration {
|
public struct PremiumConfiguration {
|
||||||
public static var defaultValue: PremiumConfiguration {
|
public static var defaultValue: PremiumConfiguration {
|
||||||
return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3)
|
return PremiumConfiguration(isPremiumDisabled: false, showPremiumGiftInAttachMenu: false, showPremiumGiftInTextField: false, giveawayGiftsPurchaseAvailable: false, boostsPerGiftCount: 3, minChannelNameColorLevel: 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
public let isPremiumDisabled: Bool
|
public let isPremiumDisabled: Bool
|
||||||
@ -1072,13 +1072,15 @@ public struct PremiumConfiguration {
|
|||||||
public let showPremiumGiftInTextField: Bool
|
public let showPremiumGiftInTextField: Bool
|
||||||
public let giveawayGiftsPurchaseAvailable: Bool
|
public let giveawayGiftsPurchaseAvailable: Bool
|
||||||
public let boostsPerGiftCount: Int32
|
public let boostsPerGiftCount: Int32
|
||||||
|
public let minChannelNameColorLevel: Int32
|
||||||
|
|
||||||
fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32) {
|
fileprivate init(isPremiumDisabled: Bool, showPremiumGiftInAttachMenu: Bool, showPremiumGiftInTextField: Bool, giveawayGiftsPurchaseAvailable: Bool, boostsPerGiftCount: Int32, minChannelNameColorLevel: Int32) {
|
||||||
self.isPremiumDisabled = isPremiumDisabled
|
self.isPremiumDisabled = isPremiumDisabled
|
||||||
self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu
|
self.showPremiumGiftInAttachMenu = showPremiumGiftInAttachMenu
|
||||||
self.showPremiumGiftInTextField = showPremiumGiftInTextField
|
self.showPremiumGiftInTextField = showPremiumGiftInTextField
|
||||||
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
|
self.giveawayGiftsPurchaseAvailable = giveawayGiftsPurchaseAvailable
|
||||||
self.boostsPerGiftCount = boostsPerGiftCount
|
self.boostsPerGiftCount = boostsPerGiftCount
|
||||||
|
self.minChannelNameColorLevel = minChannelNameColorLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration {
|
public static func with(appConfiguration: AppConfiguration) -> PremiumConfiguration {
|
||||||
@ -1088,7 +1090,8 @@ public struct PremiumConfiguration {
|
|||||||
showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? false,
|
showPremiumGiftInAttachMenu: data["premium_gift_attach_menu_icon"] as? Bool ?? false,
|
||||||
showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false,
|
showPremiumGiftInTextField: data["premium_gift_text_field_icon"] as? Bool ?? false,
|
||||||
giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false,
|
giveawayGiftsPurchaseAvailable: data["giveaway_gifts_purchase_available"] as? Bool ?? false,
|
||||||
boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3)
|
boostsPerGiftCount: Int32(data["boosts_per_sent_gift"] as? Double ?? 3),
|
||||||
|
minChannelNameColorLevel: Int32(data["channel_color_level_min"] as? Double ?? 5)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return .defaultValue
|
return .defaultValue
|
||||||
|
@ -666,6 +666,13 @@ public final class DatePickerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let date = calendar.date(from: dateComponents), date > self.maximumDate {
|
||||||
|
let maximumDateComponents = calendar.dateComponents([.hour, .minute, .day, .month, .year], from: self.maximumDate)
|
||||||
|
if let hour = maximumDateComponents.hour {
|
||||||
|
dateComponents.hour = hour - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let date = calendar.date(from: dateComponents), date >= self.minimumDate && date < self.maximumDate {
|
if let date = calendar.date(from: dateComponents), date >= self.minimumDate && date < self.maximumDate {
|
||||||
let updatedState = State(minDate: self.state.minDate, maxDate: self.state.maxDate, date: date, displayingMonthSelection: self.state.displayingMonthSelection, displayingDateSelection: self.state.displayingDateSelection, displayingTimeSelection: self.state.displayingTimeSelection, selectedMonth: monthNode.month)
|
let updatedState = State(minDate: self.state.minDate, maxDate: self.state.maxDate, date: date, displayingMonthSelection: self.state.displayingMonthSelection, displayingDateSelection: self.state.displayingDateSelection, displayingTimeSelection: self.state.displayingTimeSelection, selectedMonth: monthNode.month)
|
||||||
self.updateState(updatedState, animated: false)
|
self.updateState(updatedState, animated: false)
|
||||||
|
@ -896,58 +896,58 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
|||||||
|
|
||||||
buyActionImpl = { [weak controller] in
|
buyActionImpl = { [weak controller] in
|
||||||
let state = stateValue.with { $0 }
|
let state = stateValue.with { $0 }
|
||||||
guard let products = productsValue.with({ $0 }), !products.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|
||||||
var selectedProduct: PremiumGiftProduct?
|
|
||||||
let selectedMonths = state.selectedMonths ?? 12
|
|
||||||
switch state.mode {
|
|
||||||
case .giveaway:
|
|
||||||
if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) {
|
|
||||||
selectedProduct = product
|
|
||||||
}
|
|
||||||
case .gift:
|
|
||||||
if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == 1 }) {
|
|
||||||
selectedProduct = product
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let selectedProduct else {
|
|
||||||
let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: {
|
|
||||||
updateState { state in
|
|
||||||
var updatedState = state
|
|
||||||
updatedState.subscriptions = 25
|
|
||||||
return updatedState
|
|
||||||
}
|
|
||||||
})], parseMarkdown: true)
|
|
||||||
presentControllerImpl?(alertController)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
|
|
||||||
|
|
||||||
let purpose: AppStoreTransactionPurpose
|
|
||||||
let quantity: Int32
|
|
||||||
switch state.mode {
|
|
||||||
case .giveaway:
|
|
||||||
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
|
|
||||||
quantity = selectedProduct.giftOption.storeQuantity
|
|
||||||
case .gift:
|
|
||||||
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
|
|
||||||
quantity = Int32(state.peers.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState { state in
|
|
||||||
var updatedState = state
|
|
||||||
updatedState.updating = true
|
|
||||||
return updatedState
|
|
||||||
}
|
|
||||||
|
|
||||||
switch subject {
|
switch subject {
|
||||||
case .generic:
|
case .generic:
|
||||||
|
guard let products = productsValue.with({ $0 }), !products.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var selectedProduct: PremiumGiftProduct?
|
||||||
|
let selectedMonths = state.selectedMonths ?? 12
|
||||||
|
switch state.mode {
|
||||||
|
case .giveaway:
|
||||||
|
if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == state.subscriptions }) {
|
||||||
|
selectedProduct = product
|
||||||
|
}
|
||||||
|
case .gift:
|
||||||
|
if let product = products.first(where: { $0.months == selectedMonths && $0.giftOption.users == 1 }) {
|
||||||
|
selectedProduct = product
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let selectedProduct else {
|
||||||
|
let alertController = textAlertController(context: context, title: presentationData.strings.BoostGift_ReduceQuantity_Title, text: presentationData.strings.BoostGift_ReduceQuantity_Text("\(state.subscriptions)", "\(selectedMonths)", "\(25)").string, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.BoostGift_ReduceQuantity_Reduce, action: {
|
||||||
|
updateState { state in
|
||||||
|
var updatedState = state
|
||||||
|
updatedState.subscriptions = 25
|
||||||
|
return updatedState
|
||||||
|
}
|
||||||
|
})], parseMarkdown: true)
|
||||||
|
presentControllerImpl?(alertController)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState { state in
|
||||||
|
var updatedState = state
|
||||||
|
updatedState.updating = true
|
||||||
|
return updatedState
|
||||||
|
}
|
||||||
|
|
||||||
|
let (currency, amount) = selectedProduct.storeProduct.priceCurrencyAndAmount
|
||||||
|
|
||||||
|
let purpose: AppStoreTransactionPurpose
|
||||||
|
let quantity: Int32
|
||||||
|
switch state.mode {
|
||||||
|
case .giveaway:
|
||||||
|
purpose = .giveaway(boostPeer: peerId, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time, currency: currency, amount: amount)
|
||||||
|
quantity = selectedProduct.giftOption.storeQuantity
|
||||||
|
case .gift:
|
||||||
|
purpose = .giftCode(peerIds: state.peers, boostPeer: peerId, currency: currency, amount: amount)
|
||||||
|
quantity = Int32(state.peers.count)
|
||||||
|
}
|
||||||
|
|
||||||
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|
let _ = (context.engine.payments.canPurchasePremium(purpose: purpose)
|
||||||
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
|
|> deliverOnMainQueue).startStandalone(next: { [weak controller] available in
|
||||||
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
|
if available, let inAppPurchaseManager = context.inAppPurchaseManager {
|
||||||
@ -1033,6 +1033,12 @@ public func createGiveawayController(context: AccountContext, updatedPresentatio
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
case let .prepaid(prepaidGiveaway):
|
case let .prepaid(prepaidGiveaway):
|
||||||
|
updateState { state in
|
||||||
|
var updatedState = state
|
||||||
|
updatedState.updating = true
|
||||||
|
return updatedState
|
||||||
|
}
|
||||||
|
|
||||||
let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time)
|
let _ = (context.engine.payments.launchPrepaidGiveaway(peerId: peerId, id: prepaidGiveaway.id, additionalPeerIds: state.channels.filter { $0 != peerId }, countries: state.countries, onlyNewSubscribers: state.onlyNewEligible, randomId: Int64.random(in: .min ..< .max), untilDate: state.time)
|
||||||
|> deliverOnMainQueue).startStandalone(completed: {
|
|> deliverOnMainQueue).startStandalone(completed: {
|
||||||
if let controller, let navigationController = controller.navigationController as? NavigationController {
|
if let controller, let navigationController = controller.navigationController as? NavigationController {
|
||||||
|
@ -57,7 +57,10 @@ public func presentGiveawayInfoController(
|
|||||||
|
|
||||||
switch giveawayInfo {
|
switch giveawayInfo {
|
||||||
case let .ongoing(start, status):
|
case let .ongoing(start, status):
|
||||||
let startDate = stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
|
let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate(
|
||||||
|
stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat),
|
||||||
|
stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
|
||||||
|
).string.trimmingCharacters(in: CharacterSet(charactersIn: "*"))
|
||||||
|
|
||||||
title = presentationData.strings.Chat_Giveaway_Info_Title
|
title = presentationData.strings.Chat_Giveaway_Info_Title
|
||||||
|
|
||||||
@ -123,7 +126,11 @@ public func presentGiveawayInfoController(
|
|||||||
|
|
||||||
text = "\(intro)\n\n\(ending)\(participation)"
|
text = "\(intro)\n\n\(ending)\(participation)"
|
||||||
case let .finished(status, start, finish, _, activatedCount):
|
case let .finished(status, start, finish, _, activatedCount):
|
||||||
let startDate = stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
|
let startDate = presentationData.strings.Chat_Giveaway_Info_FullDate(
|
||||||
|
stringForMessageTimestamp(timestamp: start, dateTimeFormat: presentationData.dateTimeFormat),
|
||||||
|
stringForDate(timestamp: start, timeZone: timeZone, strings: presentationData.strings)
|
||||||
|
).string.trimmingCharacters(in: CharacterSet(charactersIn: "*"))
|
||||||
|
|
||||||
let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings)
|
let finishDate = stringForDate(timestamp: finish, timeZone: timeZone, strings: presentationData.strings)
|
||||||
title = presentationData.strings.Chat_Giveaway_Info_EndedTitle
|
title = presentationData.strings.Chat_Giveaway_Info_EndedTitle
|
||||||
|
|
||||||
|
@ -517,6 +517,10 @@ public class PremiumLimitDisplayComponent: Component {
|
|||||||
countWidth = 51.0
|
countWidth = 51.0
|
||||||
case 4:
|
case 4:
|
||||||
countWidth = 60.0
|
countWidth = 60.0
|
||||||
|
case 5:
|
||||||
|
countWidth = 74.0
|
||||||
|
case 6:
|
||||||
|
countWidth = 88.0
|
||||||
default:
|
default:
|
||||||
countWidth = 51.0
|
countWidth = 51.0
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import ShareController
|
|||||||
import ItemListPeerActionItem
|
import ItemListPeerActionItem
|
||||||
import PremiumUI
|
import PremiumUI
|
||||||
|
|
||||||
private let maxUsersDisplayedLimit: Int32 = 5
|
private let initialBoostersDisplayedLimit: Int32 = 5
|
||||||
|
|
||||||
private final class ChannelStatsControllerArguments {
|
private final class ChannelStatsControllerArguments {
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
@ -652,17 +652,20 @@ public enum ChannelStatsSection {
|
|||||||
private struct ChannelStatsControllerState: Equatable {
|
private struct ChannelStatsControllerState: Equatable {
|
||||||
let section: ChannelStatsSection
|
let section: ChannelStatsSection
|
||||||
let boostersExpanded: Bool
|
let boostersExpanded: Bool
|
||||||
|
let moreBoostersDisplayed: Int32
|
||||||
let giftsSelected: Bool
|
let giftsSelected: Bool
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.section = .stats
|
self.section = .stats
|
||||||
self.boostersExpanded = false
|
self.boostersExpanded = false
|
||||||
|
self.moreBoostersDisplayed = 0
|
||||||
self.giftsSelected = false
|
self.giftsSelected = false
|
||||||
}
|
}
|
||||||
|
|
||||||
init(section: ChannelStatsSection, boostersExpanded: Bool, giftsSelected: Bool) {
|
init(section: ChannelStatsSection, boostersExpanded: Bool, moreBoostersDisplayed: Int32, giftsSelected: Bool) {
|
||||||
self.section = section
|
self.section = section
|
||||||
self.boostersExpanded = boostersExpanded
|
self.boostersExpanded = boostersExpanded
|
||||||
|
self.moreBoostersDisplayed = moreBoostersDisplayed
|
||||||
self.giftsSelected = giftsSelected
|
self.giftsSelected = giftsSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -673,6 +676,9 @@ private struct ChannelStatsControllerState: Equatable {
|
|||||||
if lhs.boostersExpanded != rhs.boostersExpanded {
|
if lhs.boostersExpanded != rhs.boostersExpanded {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.moreBoostersDisplayed != rhs.moreBoostersDisplayed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.giftsSelected != rhs.giftsSelected {
|
if lhs.giftsSelected != rhs.giftsSelected {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -680,15 +686,19 @@ private struct ChannelStatsControllerState: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState {
|
func withUpdatedSection(_ section: ChannelStatsSection) -> ChannelStatsControllerState {
|
||||||
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, giftsSelected: self.giftsSelected)
|
return ChannelStatsControllerState(section: section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState {
|
func withUpdatedBoostersExpanded(_ boostersExpanded: Bool) -> ChannelStatsControllerState {
|
||||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, giftsSelected: self.giftsSelected)
|
return ChannelStatsControllerState(section: self.section, boostersExpanded: boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: self.giftsSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withUpdatedMoreBoostersDisplayed(_ moreBoostersDisplayed: Int32) -> ChannelStatsControllerState {
|
||||||
|
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: moreBoostersDisplayed, giftsSelected: self.giftsSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState {
|
func withUpdatedGiftsSelected(_ giftsSelected: Bool) -> ChannelStatsControllerState {
|
||||||
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, giftsSelected: giftsSelected)
|
return ChannelStatsControllerState(section: self.section, boostersExpanded: self.boostersExpanded, moreBoostersDisplayed: self.moreBoostersDisplayed, giftsSelected: giftsSelected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -826,20 +836,32 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
|
|||||||
var boosterIndex: Int32 = 0
|
var boosterIndex: Int32 = 0
|
||||||
|
|
||||||
var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts
|
var boosters: [ChannelBoostersContext.State.Boost] = selectedState.boosts
|
||||||
var effectiveExpanded = state.boostersExpanded
|
|
||||||
if boosters.count > maxUsersDisplayedLimit && !state.boostersExpanded {
|
var limit: Int32
|
||||||
boosters = Array(boosters.prefix(Int(maxUsersDisplayedLimit)))
|
if state.boostersExpanded {
|
||||||
|
limit = 25 + state.moreBoostersDisplayed
|
||||||
} else {
|
} else {
|
||||||
effectiveExpanded = true
|
limit = initialBoostersDisplayedLimit
|
||||||
}
|
}
|
||||||
|
boosters = Array(boosters.prefix(Int(limit)))
|
||||||
|
|
||||||
for booster in boosters {
|
for booster in boosters {
|
||||||
entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster))
|
entries.append(.booster(boosterIndex, presentationData.theme, presentationData.dateTimeFormat, booster))
|
||||||
boosterIndex += 1
|
boosterIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if !effectiveExpanded {
|
let totalBoostsCount = boosters.reduce(Int32(0)) { partialResult, boost in
|
||||||
entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(Int32(selectedState.count) - maxUsersDisplayedLimit)))
|
return partialResult + boost.multiplier
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalBoostsCount < selectedState.count {
|
||||||
|
let moreCount: Int32
|
||||||
|
if !state.boostersExpanded {
|
||||||
|
moreCount = min(80, selectedState.count - totalBoostsCount)
|
||||||
|
} else {
|
||||||
|
moreCount = min(200, selectedState.count - totalBoostsCount)
|
||||||
|
}
|
||||||
|
entries.append(.boostersExpand(presentationData.theme, presentationData.strings.Stats_Boosts_ShowMoreBoosts(moreCount)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -862,8 +884,8 @@ private func channelStatsControllerEntries(state: ChannelStatsControllerState, p
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController {
|
public func channelStatsController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, section: ChannelStatsSection = .stats, boostStatus: ChannelBoostStatus? = nil, statsDatacenterId: Int32?) -> ViewController {
|
||||||
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false), ignoreRepeated: true)
|
let statePromise = ValuePromise(ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false), ignoreRepeated: true)
|
||||||
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, giftsSelected: false))
|
let stateValue = Atomic(value: ChannelStatsControllerState(section: section, boostersExpanded: false, moreBoostersDisplayed: 0, giftsSelected: false))
|
||||||
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
|
let updateState: ((ChannelStatsControllerState) -> ChannelStatsControllerState) -> Void = { f in
|
||||||
statePromise.set(stateValue.modify { f($0) })
|
statePromise.set(stateValue.modify { f($0) })
|
||||||
}
|
}
|
||||||
@ -993,7 +1015,20 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||||||
pushImpl?(controller)
|
pushImpl?(controller)
|
||||||
},
|
},
|
||||||
expandBoosters: {
|
expandBoosters: {
|
||||||
updateState { $0.withUpdatedBoostersExpanded(true) }
|
var giftsSelected = false
|
||||||
|
updateState { state in
|
||||||
|
giftsSelected = state.giftsSelected
|
||||||
|
if state.boostersExpanded {
|
||||||
|
return state.withUpdatedMoreBoostersDisplayed(state.moreBoostersDisplayed + 50)
|
||||||
|
} else {
|
||||||
|
return state.withUpdatedBoostersExpanded(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if giftsSelected {
|
||||||
|
giftsContext.loadMore()
|
||||||
|
} else {
|
||||||
|
boostsContext.loadMore()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
openGifts: {
|
openGifts: {
|
||||||
let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic)
|
let controller = createGiveawayController(context: context, peerId: peerId, subject: .generic)
|
||||||
@ -1075,12 +1110,6 @@ public func channelStatsController(context: AccountContext, updatedPresentationD
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
controller.visibleBottomContentOffsetChanged = { offset in
|
|
||||||
let state = stateValue.with { $0 }
|
|
||||||
if case let .known(value) = offset, value < 510.0, case .boosts = state.section, state.boostersExpanded {
|
|
||||||
boostsContext.loadMore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controller.titleControlValueChanged = { value in
|
controller.titleControlValueChanged = { value in
|
||||||
updateState { $0.withUpdatedSection(value == 1 ? .boosts : .stats) }
|
updateState { $0.withUpdatedSection(value == 1 ? .boosts : .stats) }
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
public let maxGiveawayChannelsCount: Int32
|
public let maxGiveawayChannelsCount: Int32
|
||||||
public let maxGiveawayCountriesCount: Int32
|
public let maxGiveawayCountriesCount: Int32
|
||||||
public let maxGiveawayPeriodSeconds: Int32
|
public let maxGiveawayPeriodSeconds: Int32
|
||||||
public let minChannelNameColorLevel: Int32
|
|
||||||
|
|
||||||
public static var defaultValue: UserLimitsConfiguration {
|
public static var defaultValue: UserLimitsConfiguration {
|
||||||
return UserLimitsConfiguration(
|
return UserLimitsConfiguration(
|
||||||
@ -51,8 +50,7 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
maxStoriesSuggestedReactions: 1,
|
maxStoriesSuggestedReactions: 1,
|
||||||
maxGiveawayChannelsCount: 10,
|
maxGiveawayChannelsCount: 10,
|
||||||
maxGiveawayCountriesCount: 10,
|
maxGiveawayCountriesCount: 10,
|
||||||
maxGiveawayPeriodSeconds: 86400 * 7,
|
maxGiveawayPeriodSeconds: 86400 * 7
|
||||||
minChannelNameColorLevel: 10
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +77,7 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
maxStoriesSuggestedReactions: Int32,
|
maxStoriesSuggestedReactions: Int32,
|
||||||
maxGiveawayChannelsCount: Int32,
|
maxGiveawayChannelsCount: Int32,
|
||||||
maxGiveawayCountriesCount: Int32,
|
maxGiveawayCountriesCount: Int32,
|
||||||
maxGiveawayPeriodSeconds: Int32,
|
maxGiveawayPeriodSeconds: Int32
|
||||||
minChannelNameColorLevel: Int32
|
|
||||||
) {
|
) {
|
||||||
self.maxPinnedChatCount = maxPinnedChatCount
|
self.maxPinnedChatCount = maxPinnedChatCount
|
||||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||||
@ -105,7 +102,6 @@ public struct UserLimitsConfiguration: Equatable {
|
|||||||
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
|
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
|
||||||
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
|
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
|
||||||
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
|
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
|
||||||
self.minChannelNameColorLevel = minChannelNameColorLevel
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +149,5 @@ extension UserLimitsConfiguration {
|
|||||||
self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount)
|
self.maxGiveawayChannelsCount = getGeneralValue("giveaway_add_peers_max", orElse: defaultValue.maxGiveawayChannelsCount)
|
||||||
self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount)
|
self.maxGiveawayCountriesCount = getGeneralValue("giveaway_countries_max", orElse: defaultValue.maxGiveawayCountriesCount)
|
||||||
self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds)
|
self.maxGiveawayPeriodSeconds = getGeneralValue("giveaway_period_max", orElse: defaultValue.maxGiveawayPeriodSeconds)
|
||||||
self.minChannelNameColorLevel = getGeneralValue("channel_color_level_min", orElse: defaultValue.minChannelNameColorLevel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,6 @@ public enum EngineConfiguration {
|
|||||||
public let maxGiveawayChannelsCount: Int32
|
public let maxGiveawayChannelsCount: Int32
|
||||||
public let maxGiveawayCountriesCount: Int32
|
public let maxGiveawayCountriesCount: Int32
|
||||||
public let maxGiveawayPeriodSeconds: Int32
|
public let maxGiveawayPeriodSeconds: Int32
|
||||||
public let minChannelNameColorLevel: Int32
|
|
||||||
|
|
||||||
public static var defaultValue: UserLimits {
|
public static var defaultValue: UserLimits {
|
||||||
return UserLimits(UserLimitsConfiguration.defaultValue)
|
return UserLimits(UserLimitsConfiguration.defaultValue)
|
||||||
@ -88,8 +87,7 @@ public enum EngineConfiguration {
|
|||||||
maxStoriesSuggestedReactions: Int32,
|
maxStoriesSuggestedReactions: Int32,
|
||||||
maxGiveawayChannelsCount: Int32,
|
maxGiveawayChannelsCount: Int32,
|
||||||
maxGiveawayCountriesCount: Int32,
|
maxGiveawayCountriesCount: Int32,
|
||||||
maxGiveawayPeriodSeconds: Int32,
|
maxGiveawayPeriodSeconds: Int32
|
||||||
minChannelNameColorLevel: Int32
|
|
||||||
) {
|
) {
|
||||||
self.maxPinnedChatCount = maxPinnedChatCount
|
self.maxPinnedChatCount = maxPinnedChatCount
|
||||||
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
self.maxArchivedPinnedChatCount = maxArchivedPinnedChatCount
|
||||||
@ -114,7 +112,6 @@ public enum EngineConfiguration {
|
|||||||
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
|
self.maxGiveawayChannelsCount = maxGiveawayChannelsCount
|
||||||
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
|
self.maxGiveawayCountriesCount = maxGiveawayCountriesCount
|
||||||
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
|
self.maxGiveawayPeriodSeconds = maxGiveawayPeriodSeconds
|
||||||
self.minChannelNameColorLevel = minChannelNameColorLevel
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,8 +171,7 @@ public extension EngineConfiguration.UserLimits {
|
|||||||
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions,
|
maxStoriesSuggestedReactions: userLimitsConfiguration.maxStoriesSuggestedReactions,
|
||||||
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount,
|
maxGiveawayChannelsCount: userLimitsConfiguration.maxGiveawayChannelsCount,
|
||||||
maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount,
|
maxGiveawayCountriesCount: userLimitsConfiguration.maxGiveawayCountriesCount,
|
||||||
maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds,
|
maxGiveawayPeriodSeconds: userLimitsConfiguration.maxGiveawayPeriodSeconds
|
||||||
minChannelNameColorLevel: userLimitsConfiguration.minChannelNameColorLevel
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import ChatPresentationInterfaceState
|
|||||||
import ShimmerEffect
|
import ShimmerEffect
|
||||||
|
|
||||||
private let buttonFont = Font.semibold(14.0)
|
private let buttonFont = Font.semibold(14.0)
|
||||||
private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate)
|
private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 6.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate)
|
||||||
|
|
||||||
public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
|
public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
@ -60,7 +60,7 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton
|
|||||||
shimmerEffectNode = current
|
shimmerEffectNode = current
|
||||||
} else {
|
} else {
|
||||||
shimmerEffectNode = ShimmerEffectForegroundNode()
|
shimmerEffectNode = ShimmerEffectForegroundNode()
|
||||||
shimmerEffectNode.cornerRadius = 5.0
|
shimmerEffectNode.cornerRadius = 6.0
|
||||||
self.insertSubnode(shimmerEffectNode, at: 0)
|
self.insertSubnode(shimmerEffectNode, at: 0)
|
||||||
self.shimmerEffectNode = shimmerEffectNode
|
self.shimmerEffectNode = shimmerEffectNode
|
||||||
}
|
}
|
||||||
|
@ -850,6 +850,11 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let _ = message.adAttribute {
|
||||||
|
actualSize.height += 2.0
|
||||||
|
backgroundInsets.bottom += 2.0
|
||||||
|
}
|
||||||
|
|
||||||
return (actualSize, { animation, synchronousLoads, applyInfo in
|
return (actualSize, { animation, synchronousLoads, applyInfo in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -1214,890 +1219,6 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/*var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0)
|
|
||||||
if displayLine {
|
|
||||||
horizontalInsets.left += 10.0
|
|
||||||
horizontalInsets.right += 9.0
|
|
||||||
}
|
|
||||||
|
|
||||||
var titleBeforeMedia = false
|
|
||||||
var preferMediaBeforeText = false
|
|
||||||
var preferMediaAspectFilled = false
|
|
||||||
if let (_, flags) = mediaAndFlags {
|
|
||||||
preferMediaBeforeText = flags.contains(.preferMediaBeforeText)
|
|
||||||
preferMediaAspectFilled = flags.contains(.preferMediaAspectFilled)
|
|
||||||
titleBeforeMedia = flags.contains(.titleBeforeMedia)
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit
|
|
||||||
|
|
||||||
var edited = false
|
|
||||||
if attributes.updatingMedia != nil {
|
|
||||||
edited = true
|
|
||||||
}
|
|
||||||
var viewCount: Int?
|
|
||||||
var dateReplies = 0
|
|
||||||
var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message)
|
|
||||||
if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) {
|
|
||||||
dateReactionsAndPeers = ([], [])
|
|
||||||
}
|
|
||||||
for attribute in message.attributes {
|
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
|
||||||
edited = !attribute.isHidden
|
|
||||||
} else if let attribute = attribute as? ViewCountMessageAttribute {
|
|
||||||
viewCount = attribute.count
|
|
||||||
} else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation {
|
|
||||||
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
|
||||||
dateReplies = Int(attribute.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData)
|
|
||||||
|
|
||||||
var webpageGalleryMediaCount: Int?
|
|
||||||
for media in message.media {
|
|
||||||
if let media = media as? TelegramMediaWebpage {
|
|
||||||
if case let .Loaded(content) = media.content, let instantPage = content.instantPage, let image = content.image {
|
|
||||||
switch instantPageType(of: content) {
|
|
||||||
case .album:
|
|
||||||
let count = instantPageGalleryMedia(webpageId: media.webpageId, page: instantPage, galleryMedia: image).count
|
|
||||||
if count > 1 {
|
|
||||||
webpageGalleryMediaCount = count
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var textString: NSAttributedString?
|
|
||||||
var inlineImageDimensions: CGSize?
|
|
||||||
var inlineImageSize: CGSize?
|
|
||||||
var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
|
|
||||||
var textCutout = TextNodeCutout()
|
|
||||||
var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude
|
|
||||||
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))?
|
|
||||||
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)))?
|
|
||||||
|
|
||||||
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)?
|
|
||||||
|
|
||||||
let topTitleString = NSMutableAttributedString()
|
|
||||||
|
|
||||||
let string = NSMutableAttributedString()
|
|
||||||
var notEmpty = false
|
|
||||||
|
|
||||||
let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing
|
|
||||||
|
|
||||||
if let title = title, !title.isEmpty {
|
|
||||||
if titleBeforeMedia {
|
|
||||||
topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor))
|
|
||||||
} else {
|
|
||||||
string.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor))
|
|
||||||
notEmpty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let subtitle = subtitle, subtitle.length > 0 {
|
|
||||||
if notEmpty {
|
|
||||||
string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor))
|
|
||||||
}
|
|
||||||
let updatedSubtitle = NSMutableAttributedString()
|
|
||||||
updatedSubtitle.append(subtitle)
|
|
||||||
updatedSubtitle.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length))
|
|
||||||
updatedSubtitle.addAttribute(.font, value: titleFont, range: NSMakeRange(0, subtitle.length))
|
|
||||||
string.append(updatedSubtitle)
|
|
||||||
notEmpty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if let text = text, !text.isEmpty {
|
|
||||||
if notEmpty {
|
|
||||||
string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor))
|
|
||||||
}
|
|
||||||
if let entities = entities {
|
|
||||||
string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true))
|
|
||||||
} else {
|
|
||||||
string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: messageTheme.primaryTextColor))
|
|
||||||
}
|
|
||||||
notEmpty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
textString = string
|
|
||||||
if string.length > 1000 {
|
|
||||||
textString = string.attributedSubstring(from: NSMakeRange(0, 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
var isReplyThread = false
|
|
||||||
if case .replyThread = chatLocation {
|
|
||||||
isReplyThread = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var skipStandardStatus = false
|
|
||||||
var isImage = false
|
|
||||||
var isFile = false
|
|
||||||
|
|
||||||
var automaticPlayback = false
|
|
||||||
|
|
||||||
var textStatusType: ChatMessageDateAndStatusType?
|
|
||||||
var imageStatusType: ChatMessageDateAndStatusType?
|
|
||||||
var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent?
|
|
||||||
|
|
||||||
if let (media, flags) = mediaAndFlags {
|
|
||||||
if let file = media as? TelegramMediaFile {
|
|
||||||
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
|
||||||
isImage = true
|
|
||||||
} else if file.isInstantVideo {
|
|
||||||
isImage = true
|
|
||||||
} else if file.isVideo {
|
|
||||||
isImage = true
|
|
||||||
} else if file.isSticker || file.isAnimatedSticker {
|
|
||||||
isImage = true
|
|
||||||
} else {
|
|
||||||
isFile = true
|
|
||||||
}
|
|
||||||
} else if let _ = media as? TelegramMediaImage {
|
|
||||||
if !flags.contains(.preferMediaInline) {
|
|
||||||
isImage = true
|
|
||||||
}
|
|
||||||
} else if let _ = media as? TelegramMediaWebFile {
|
|
||||||
isImage = true
|
|
||||||
} else if let _ = media as? WallpaperPreviewMedia {
|
|
||||||
isImage = true
|
|
||||||
} else if let _ = media as? TelegramMediaStory {
|
|
||||||
isImage = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if preferMediaBeforeText, let textString, textString.length != 0 {
|
|
||||||
isImage = false
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusInText = !isImage
|
|
||||||
if let textString {
|
|
||||||
if textString.length == 0 {
|
|
||||||
statusInText = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
statusInText = false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch preparePosition {
|
|
||||||
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
|
|
||||||
if let count = webpageGalleryMediaCount {
|
|
||||||
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil)
|
|
||||||
skipStandardStatus = isImage
|
|
||||||
} else if let mediaBadge = mediaBadge {
|
|
||||||
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil)
|
|
||||||
} else {
|
|
||||||
skipStandardStatus = isFile
|
|
||||||
}
|
|
||||||
|
|
||||||
if !skipStandardStatus {
|
|
||||||
if incoming {
|
|
||||||
if isImage {
|
|
||||||
imageStatusType = .ImageIncoming
|
|
||||||
} else {
|
|
||||||
textStatusType = .BubbleIncoming
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if message.flags.contains(.Failed) {
|
|
||||||
if isImage {
|
|
||||||
imageStatusType = .ImageOutgoing(.Failed)
|
|
||||||
} else {
|
|
||||||
textStatusType = .BubbleOutgoing(.Failed)
|
|
||||||
}
|
|
||||||
} else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil {
|
|
||||||
if isImage {
|
|
||||||
imageStatusType = .ImageOutgoing(.Sending)
|
|
||||||
} else {
|
|
||||||
textStatusType = .BubbleOutgoing(.Sending)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if isImage {
|
|
||||||
imageStatusType = .ImageOutgoing(.Sent(read: messageRead))
|
|
||||||
} else {
|
|
||||||
textStatusType = .BubbleOutgoing(.Sent(read: messageRead))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in
|
|
||||||
ChatMessageDateAndStatus(
|
|
||||||
type: statusType,
|
|
||||||
edited: edited,
|
|
||||||
viewCount: viewCount,
|
|
||||||
dateReactions: dateReactionsAndPeers.reactions,
|
|
||||||
dateReactionPeers: dateReactionsAndPeers.peers,
|
|
||||||
dateReplies: dateReplies,
|
|
||||||
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
||||||
dateText: dateText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (media, flags) = mediaAndFlags {
|
|
||||||
if let file = media as? TelegramMediaFile {
|
|
||||||
if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 {
|
|
||||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
||||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
||||||
refineContentImageLayout = refineLayout
|
|
||||||
} else if file.isInstantVideo {
|
|
||||||
let displaySize = CGSize(width: 212.0, height: 212.0)
|
|
||||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
|
||||||
let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, topMessage: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload, 0.0)
|
|
||||||
initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight
|
|
||||||
contentInstantVideoSizeAndApply = (videoLayout, apply)
|
|
||||||
} else if file.isVideo {
|
|
||||||
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
|
|
||||||
|
|
||||||
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) {
|
|
||||||
automaticDownload = .full
|
|
||||||
} else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) {
|
|
||||||
automaticDownload = .prefetch
|
|
||||||
}
|
|
||||||
if file.isAnimated {
|
|
||||||
automaticPlayback = context.sharedContext.energyUsageSettings.autoplayGif
|
|
||||||
} else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo {
|
|
||||||
var willDownloadOrLocal = false
|
|
||||||
if case .full = automaticDownload {
|
|
||||||
willDownloadOrLocal = true
|
|
||||||
} else {
|
|
||||||
willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil
|
|
||||||
}
|
|
||||||
if willDownloadOrLocal {
|
|
||||||
automaticPlayback = true
|
|
||||||
contentMode = .aspectFill
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
||||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
||||||
refineContentImageLayout = refineLayout
|
|
||||||
} else if file.isSticker || file.isAnimatedSticker {
|
|
||||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
|
||||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
||||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
||||||
refineContentImageLayout = refineLayout
|
|
||||||
} else {
|
|
||||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
|
|
||||||
|
|
||||||
let statusType: ChatMessageDateAndStatusType
|
|
||||||
if incoming {
|
|
||||||
statusType = .BubbleIncoming
|
|
||||||
} else {
|
|
||||||
if message.flags.contains(.Failed) {
|
|
||||||
statusType = .BubbleOutgoing(.Failed)
|
|
||||||
} else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil {
|
|
||||||
statusType = .BubbleOutgoing(.Sending)
|
|
||||||
} else {
|
|
||||||
statusType = .BubbleOutgoing(.Sent(read: messageRead))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments(
|
|
||||||
context: context,
|
|
||||||
presentationData: presentationData,
|
|
||||||
message: message,
|
|
||||||
topMessage: message,
|
|
||||||
associatedData: associatedData,
|
|
||||||
chatLocation: chatLocation,
|
|
||||||
attributes: attributes,
|
|
||||||
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
||||||
forcedIsEdited: false,
|
|
||||||
file: file,
|
|
||||||
automaticDownload: automaticDownload,
|
|
||||||
incoming: incoming,
|
|
||||||
isRecentActions: false,
|
|
||||||
forcedResourceStatus: associatedData.forcedResourceStatus,
|
|
||||||
dateAndStatusType: statusType,
|
|
||||||
displayReactions: false,
|
|
||||||
messageSelection: nil,
|
|
||||||
layoutConstants: layoutConstants,
|
|
||||||
constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height),
|
|
||||||
controllerInteraction: controllerInteraction
|
|
||||||
))
|
|
||||||
refineContentFileLayout = refineLayout
|
|
||||||
}
|
|
||||||
} else if let image = media as? TelegramMediaImage {
|
|
||||||
if !flags.contains(.preferMediaInline) {
|
|
||||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
|
|
||||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
||||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
||||||
refineContentImageLayout = refineLayout
|
|
||||||
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
|
|
||||||
inlineImageDimensions = dimensions.cgSize
|
|
||||||
|
|
||||||
if image != currentImage || !currentMediaIsInline {
|
|
||||||
updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), photoReference: .message(message: MessageReference(message), media: image))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let image = media as? TelegramMediaWebFile {
|
|
||||||
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
|
|
||||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
||||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
||||||
refineContentImageLayout = refineLayout
|
|
||||||
} else if let wallpaper = media as? WallpaperPreviewMedia {
|
|
||||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
||||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
||||||
refineContentImageLayout = refineLayout
|
|
||||||
if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme {
|
|
||||||
skipStandardStatus = true
|
|
||||||
}
|
|
||||||
} else if let story = media as? TelegramMediaStory {
|
|
||||||
var media: Media?
|
|
||||||
if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue {
|
|
||||||
media = item.media
|
|
||||||
}
|
|
||||||
|
|
||||||
var automaticDownload = false
|
|
||||||
if let media {
|
|
||||||
automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media)
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext)
|
|
||||||
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
|
|
||||||
refineContentImageLayout = refineLayout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let _ = inlineImageDimensions {
|
|
||||||
inlineImageSize = CGSize(width: 53.0, height: 53.0)
|
|
||||||
|
|
||||||
if let inlineImageSize = inlineImageSize {
|
|
||||||
textCutout.topRight = CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (initialWidth, { constrainedSize, position in
|
|
||||||
var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 0.0, right: horizontalInsets.right)
|
|
||||||
|
|
||||||
switch position {
|
|
||||||
case let .linear(topNeighbor, bottomNeighbor):
|
|
||||||
switch topNeighbor {
|
|
||||||
case .None:
|
|
||||||
insets.top += 10.0
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch bottomNeighbor {
|
|
||||||
case .None:
|
|
||||||
insets.bottom += 12.0
|
|
||||||
default:
|
|
||||||
insets.bottom += 0.0
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
|
|
||||||
|
|
||||||
var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge?
|
|
||||||
if let _ = additionalImageBadgeContent {
|
|
||||||
updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge()
|
|
||||||
}
|
|
||||||
|
|
||||||
let upatedTextCutout = textCutout
|
|
||||||
|
|
||||||
|
|
||||||
let (topTitleLayout, topTitleApply) = topTitleAsyncLayout(TextNodeLayoutArguments(attributedString: topTitleString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
|
||||||
let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets()))
|
|
||||||
|
|
||||||
var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))?
|
|
||||||
if statusInText, let textStatusType = textStatusType {
|
|
||||||
let trailingContentWidth: CGFloat
|
|
||||||
if textLayout.hasRTL {
|
|
||||||
trailingContentWidth = 10000.0
|
|
||||||
} else {
|
|
||||||
trailingContentWidth = textLayout.trailingLineWidth
|
|
||||||
}
|
|
||||||
statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments(
|
|
||||||
context: context,
|
|
||||||
presentationData: presentationData,
|
|
||||||
edited: edited,
|
|
||||||
impressionCount: viewCount,
|
|
||||||
dateText: dateText,
|
|
||||||
type: textStatusType,
|
|
||||||
layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil),
|
|
||||||
constrainedSize: textConstrainedSize,
|
|
||||||
availableReactions: associatedData.availableReactions,
|
|
||||||
reactions: dateReactionsAndPeers.reactions,
|
|
||||||
reactionPeers: dateReactionsAndPeers.peers,
|
|
||||||
displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser,
|
|
||||||
replyCount: dateReplies,
|
|
||||||
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
|
||||||
hasAutoremove: message.isSelfExpiring,
|
|
||||||
canViewReactionList: canViewMessageReactionList(message: message),
|
|
||||||
animationCache: controllerInteraction.presentationContext.animationCache,
|
|
||||||
animationRenderer: controllerInteraction.presentationContext.animationRenderer
|
|
||||||
))
|
|
||||||
}
|
|
||||||
let _ = statusSuggestedWidthAndContinue
|
|
||||||
|
|
||||||
var textFrame = CGRect(origin: CGPoint(), size: textLayout.size)
|
|
||||||
|
|
||||||
textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top)
|
|
||||||
|
|
||||||
let mainColor: UIColor
|
|
||||||
if !incoming {
|
|
||||||
mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
|
||||||
} else {
|
|
||||||
var authorNameColor: UIColor?
|
|
||||||
let author = message.author
|
|
||||||
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser {
|
|
||||||
authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] }
|
|
||||||
if let rawAuthorNameColor = authorNameColor {
|
|
||||||
var dimColors = false
|
|
||||||
switch presentationData.theme.theme.name {
|
|
||||||
case .builtin(.nightAccent), .builtin(.night):
|
|
||||||
dimColors = true
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if dimColors {
|
|
||||||
var hue: CGFloat = 0.0
|
|
||||||
var saturation: CGFloat = 0.0
|
|
||||||
var brightness: CGFloat = 0.0
|
|
||||||
rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil)
|
|
||||||
authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let authorNameColor {
|
|
||||||
mainColor = authorNameColor
|
|
||||||
} else {
|
|
||||||
mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var boundingSize = textFrame.size
|
|
||||||
if titleBeforeMedia {
|
|
||||||
boundingSize.height += topTitleLayout.size.height + 4.0
|
|
||||||
boundingSize.width = max(boundingSize.width, topTitleLayout.size.width)
|
|
||||||
}
|
|
||||||
if let inlineImageSize = inlineImageSize {
|
|
||||||
if boundingSize.height < inlineImageSize.height {
|
|
||||||
boundingSize.height = inlineImageSize.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
|
||||||
boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode))?
|
|
||||||
if let refineContentImageLayout = refineContentImageLayout {
|
|
||||||
let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0))
|
|
||||||
finalizeContentImageLayout = finalizeImageLayout
|
|
||||||
|
|
||||||
boundingSize.width = max(boundingSize.width, refinedWidth)
|
|
||||||
}
|
|
||||||
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode))?
|
|
||||||
if let refineContentFileLayout = refineContentFileLayout {
|
|
||||||
let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize)
|
|
||||||
finalizeContentFileLayout = finalizeFileLayout
|
|
||||||
|
|
||||||
boundingSize.width = max(boundingSize.width, refinedWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
|
|
||||||
boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
var imageApply: (() -> Void)?
|
|
||||||
if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions {
|
|
||||||
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
|
|
||||||
let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor)
|
|
||||||
imageApply = imageLayout(arguments)
|
|
||||||
}
|
|
||||||
|
|
||||||
var continueActionButtonLayout: ((CGFloat, CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))?
|
|
||||||
if let actionTitle = actionTitle, !isPreview {
|
|
||||||
var buttonIconImage: UIImage?
|
|
||||||
var buttonHighlightedIconImage: UIImage?
|
|
||||||
var cornerIcon = false
|
|
||||||
let titleColor: UIColor
|
|
||||||
let titleHighlightedColor: UIColor
|
|
||||||
if incoming {
|
|
||||||
if let actionIcon {
|
|
||||||
switch actionIcon {
|
|
||||||
case .instant:
|
|
||||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)!
|
|
||||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
||||||
case .link:
|
|
||||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)!
|
|
||||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
||||||
cornerIcon = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
|
|
||||||
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
|
||||||
titleHighlightedColor = bubbleColor.fill[0]
|
|
||||||
} else {
|
|
||||||
if let actionIcon {
|
|
||||||
switch actionIcon {
|
|
||||||
case .instant:
|
|
||||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)!
|
|
||||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
||||||
case .link:
|
|
||||||
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)!
|
|
||||||
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
|
|
||||||
cornerIcon = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
|
||||||
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
|
|
||||||
titleHighlightedColor = bubbleColor.fill[0]
|
|
||||||
}
|
|
||||||
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, nil, buttonIconImage, buttonHighlightedIconImage, cornerIcon, actionTitle, titleColor, titleHighlightedColor, false)
|
|
||||||
boundingSize.width = max(buttonWidth, boundingSize.width)
|
|
||||||
continueActionButtonLayout = continueLayout
|
|
||||||
}
|
|
||||||
|
|
||||||
boundingSize.width += insets.left + insets.right
|
|
||||||
boundingSize.height += insets.top + insets.bottom
|
|
||||||
|
|
||||||
return (boundingSize.width, { boundingWidth in
|
|
||||||
var adjustedBoundingSize = boundingSize
|
|
||||||
|
|
||||||
var imageFrame: CGRect?
|
|
||||||
if let inlineImageSize = inlineImageSize {
|
|
||||||
imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right + 4.0, y: 0.0), size: inlineImageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentImageSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)?
|
|
||||||
if let finalizeContentImageLayout = finalizeContentImageLayout {
|
|
||||||
let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right)
|
|
||||||
contentImageSizeAndApply = (size, apply)
|
|
||||||
|
|
||||||
var imageHeightAddition = size.height
|
|
||||||
if textFrame.size.height > CGFloat.ulpOfOne {
|
|
||||||
imageHeightAddition += 2.0
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustedBoundingSize.height += imageHeightAddition + 7.0
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)?
|
|
||||||
if let finalizeContentFileLayout = finalizeContentFileLayout {
|
|
||||||
let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right)
|
|
||||||
contentFileSizeAndApply = (size, apply)
|
|
||||||
|
|
||||||
var imageHeightAddition = size.height + 6.0
|
|
||||||
if textFrame.size.height > CGFloat.ulpOfOne {
|
|
||||||
imageHeightAddition += 6.0
|
|
||||||
} else {
|
|
||||||
imageHeightAddition += 7.0
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustedBoundingSize.height += imageHeightAddition + 5.0
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
|
|
||||||
let imageHeightAddition = videoLayout.contentSize.height + 6.0
|
|
||||||
|
|
||||||
adjustedBoundingSize.height += imageHeightAddition// + 5.0
|
|
||||||
}
|
|
||||||
|
|
||||||
var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))?
|
|
||||||
if let continueActionButtonLayout = continueActionButtonLayout {
|
|
||||||
let (size, apply) = continueActionButtonLayout(boundingWidth - 5.0 - insets.right, 38.0)
|
|
||||||
actionButtonSizeAndApply = (size, apply)
|
|
||||||
adjustedBoundingSize.height += 4.0 + size.height
|
|
||||||
if let text, !text.isEmpty {
|
|
||||||
if contentImageSizeAndApply == nil {
|
|
||||||
adjustedBoundingSize.height += 5.0
|
|
||||||
} else if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
||||||
adjustedBoundingSize.height += 5.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)?
|
|
||||||
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
|
|
||||||
statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right)
|
|
||||||
}
|
|
||||||
if let statusSizeAndApply = statusSizeAndApply {
|
|
||||||
adjustedBoundingSize.height += statusSizeAndApply.0.height
|
|
||||||
|
|
||||||
if let imageFrame = imageFrame, statusSizeAndApply.0.height == 0.0 {
|
|
||||||
if statusInText {
|
|
||||||
adjustedBoundingSize.height = max(adjustedBoundingSize.height, imageFrame.maxY + 8.0 + 15.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width)
|
|
||||||
|
|
||||||
var contentMediaHeight: CGFloat?
|
|
||||||
if let (contentImageSize, _) = contentImageSizeAndApply {
|
|
||||||
contentMediaHeight = contentImageSize.height
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (contentFileSize, _) = contentFileSizeAndApply {
|
|
||||||
contentMediaHeight = contentFileSize.height
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
|
|
||||||
contentMediaHeight = videoLayout.contentSize.height
|
|
||||||
}
|
|
||||||
|
|
||||||
var textVerticalOffset: CGFloat = 0.0
|
|
||||||
if titleBeforeMedia {
|
|
||||||
textVerticalOffset += topTitleLayout.size.height + 4.0
|
|
||||||
}
|
|
||||||
if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
||||||
textVerticalOffset += contentMediaHeight + 7.0
|
|
||||||
}
|
|
||||||
let adjustedTextFrame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset)
|
|
||||||
|
|
||||||
var statusFrame: CGRect?
|
|
||||||
if let statusSizeAndApply = statusSizeAndApply {
|
|
||||||
var finalStatusFrame = CGRect(origin: CGPoint(x: adjustedTextFrame.minX, y: adjustedTextFrame.maxY), size: statusSizeAndApply.0)
|
|
||||||
if let imageFrame = imageFrame {
|
|
||||||
if finalStatusFrame.maxY < imageFrame.maxY + 10.0 {
|
|
||||||
finalStatusFrame.origin.y = max(finalStatusFrame.minY, imageFrame.maxY + 2.0)
|
|
||||||
if finalStatusFrame.height == 0.0 {
|
|
||||||
finalStatusFrame.origin.y += 14.0
|
|
||||||
|
|
||||||
adjustedBoundingSize.height += 14.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statusFrame = finalStatusFrame
|
|
||||||
}
|
|
||||||
|
|
||||||
return (adjustedBoundingSize, { [weak self] animation, synchronousLoads, applyInfo in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.context = context
|
|
||||||
strongSelf.message = message
|
|
||||||
strongSelf.media = mediaAndFlags?.0
|
|
||||||
strongSelf.theme = presentationData.theme
|
|
||||||
|
|
||||||
let backgroundView: UIImageView
|
|
||||||
if let current = strongSelf.backgroundView {
|
|
||||||
backgroundView = current
|
|
||||||
} else {
|
|
||||||
backgroundView = UIImageView()
|
|
||||||
strongSelf.backgroundView = backgroundView
|
|
||||||
strongSelf.view.insertSubview(backgroundView, at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if backgroundView.image == nil {
|
|
||||||
backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme)
|
|
||||||
}
|
|
||||||
backgroundView.tintColor = mainColor
|
|
||||||
|
|
||||||
animation.animator.updateFrame(layer: backgroundView.layer, frame: CGRect(origin: CGPoint(x: 11.0, y: insets.top - 3.0), size: CGSize(width: adjustedBoundingSize.width - 4.0 - insets.right, height: adjustedBoundingSize.height - insets.top - insets.bottom + 4.0)), completion: nil)
|
|
||||||
backgroundView.isHidden = !displayLine
|
|
||||||
|
|
||||||
//strongSelf.borderColor = UIColor.red.cgColor
|
|
||||||
//strongSelf.borderWidth = 2.0
|
|
||||||
|
|
||||||
strongSelf.textNode.textNode.displaysAsynchronously = !isPreview
|
|
||||||
|
|
||||||
let _ = topTitleApply()
|
|
||||||
strongSelf.topTitleNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: insets.top), size: topTitleLayout.size)
|
|
||||||
|
|
||||||
let _ = textApply(TextNodeWithEntities.Arguments(
|
|
||||||
context: context,
|
|
||||||
cache: animationCache,
|
|
||||||
renderer: animationRenderer,
|
|
||||||
placeholderColor: messageTheme.mediaPlaceholderColor,
|
|
||||||
attemptSynchronous: synchronousLoads
|
|
||||||
))
|
|
||||||
switch strongSelf.visibility {
|
|
||||||
case .none:
|
|
||||||
strongSelf.textNode.visibilityRect = nil
|
|
||||||
case let .visible(_, subRect):
|
|
||||||
var subRect = subRect
|
|
||||||
subRect.origin.x = 0.0
|
|
||||||
subRect.size.width = 10000.0
|
|
||||||
strongSelf.textNode.visibilityRect = subRect
|
|
||||||
}
|
|
||||||
|
|
||||||
if let imageFrame = imageFrame {
|
|
||||||
if let updateImageSignal = updateInlineImageSignal {
|
|
||||||
strongSelf.inlineImageNode.setSignal(updateImageSignal)
|
|
||||||
}
|
|
||||||
animation.animator.updateFrame(layer: strongSelf.inlineImageNode.layer, frame: imageFrame, completion: nil)
|
|
||||||
if strongSelf.inlineImageNode.supernode == nil {
|
|
||||||
strongSelf.addSubnode(strongSelf.inlineImageNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let imageApply = imageApply {
|
|
||||||
imageApply()
|
|
||||||
}
|
|
||||||
} else if strongSelf.inlineImageNode.supernode != nil {
|
|
||||||
strongSelf.inlineImageNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (contentImageSize, contentImageApply) = contentImageSizeAndApply {
|
|
||||||
let contentImageNode = contentImageApply(animation, synchronousLoads)
|
|
||||||
if strongSelf.contentImageNode !== contentImageNode {
|
|
||||||
strongSelf.contentImageNode = contentImageNode
|
|
||||||
contentImageNode.activatePinch = { sourceNode in
|
|
||||||
controllerInteraction.activateMessagePinch(sourceNode)
|
|
||||||
}
|
|
||||||
strongSelf.addSubnode(contentImageNode)
|
|
||||||
contentImageNode.activateLocalContent = { [weak strongSelf] mode in
|
|
||||||
if let strongSelf = strongSelf {
|
|
||||||
strongSelf.openMedia?(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in
|
|
||||||
guard let controllerInteraction = controllerInteraction else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
controllerInteraction.updateMessageReaction(message, value)
|
|
||||||
}
|
|
||||||
contentImageNode.visibility = strongSelf.visibility != .none
|
|
||||||
}
|
|
||||||
let _ = contentImageApply(animation, synchronousLoads)
|
|
||||||
var contentImageFrame: CGRect
|
|
||||||
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
||||||
contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize)
|
|
||||||
if titleBeforeMedia {
|
|
||||||
contentImageFrame.origin.y += topTitleLayout.size.height + 4.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentImageNode.frame = contentImageFrame
|
|
||||||
} else if let contentImageNode = strongSelf.contentImageNode {
|
|
||||||
contentImageNode.visibility = false
|
|
||||||
contentImageNode.removeFromSupernode()
|
|
||||||
strongSelf.contentImageNode = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let updatedAdditionalImageBadge = updatedAdditionalImageBadge, let contentImageNode = strongSelf.contentImageNode, let contentImageSize = contentImageSizeAndApply?.0 {
|
|
||||||
if strongSelf.additionalImageBadgeNode != updatedAdditionalImageBadge {
|
|
||||||
strongSelf.additionalImageBadgeNode?.removeFromSupernode()
|
|
||||||
}
|
|
||||||
strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge
|
|
||||||
contentImageNode.addSubnode(updatedAdditionalImageBadge)
|
|
||||||
if mediaBadge != nil {
|
|
||||||
updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, animated: false)
|
|
||||||
updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 0.0, height: 0.0))
|
|
||||||
} else {
|
|
||||||
updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, alignment: .right, animated: false)
|
|
||||||
updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 6.0, y: contentImageSize.height - 18.0 - 6.0), size: CGSize(width: 0.0, height: 0.0))
|
|
||||||
}
|
|
||||||
} else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode {
|
|
||||||
strongSelf.additionalImageBadgeNode = nil
|
|
||||||
additionalImageBadgeNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (contentFileSize, contentFileApply) = contentFileSizeAndApply {
|
|
||||||
let contentFileNode = contentFileApply(synchronousLoads, animation, applyInfo)
|
|
||||||
if strongSelf.contentFileNode !== contentFileNode {
|
|
||||||
strongSelf.contentFileNode = contentFileNode
|
|
||||||
strongSelf.addSubnode(contentFileNode)
|
|
||||||
contentFileNode.activateLocalContent = { [weak strongSelf] in
|
|
||||||
if let strongSelf = strongSelf {
|
|
||||||
strongSelf.openMedia?(.default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in
|
|
||||||
if let strongSelf = strongSelf {
|
|
||||||
strongSelf.requestUpdateLayout?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
||||||
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize)
|
|
||||||
} else {
|
|
||||||
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 8.0 : 7.0)), size: contentFileSize)
|
|
||||||
}
|
|
||||||
} else if let contentFileNode = strongSelf.contentFileNode {
|
|
||||||
contentFileNode.removeFromSupernode()
|
|
||||||
strongSelf.contentFileNode = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (videoLayout, apply) = contentInstantVideoSizeAndApply {
|
|
||||||
let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation)
|
|
||||||
if strongSelf.contentInstantVideoNode !== contentInstantVideoNode {
|
|
||||||
strongSelf.contentInstantVideoNode = contentInstantVideoNode
|
|
||||||
strongSelf.addSubnode(contentInstantVideoNode)
|
|
||||||
}
|
|
||||||
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
|
|
||||||
contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: videoLayout.contentSize)
|
|
||||||
} else {
|
|
||||||
contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: videoLayout.contentSize)
|
|
||||||
}
|
|
||||||
} else if let contentInstantVideoNode = strongSelf.contentInstantVideoNode {
|
|
||||||
contentInstantVideoNode.removeFromSupernode()
|
|
||||||
strongSelf.contentInstantVideoNode = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
strongSelf.textNode.textNode.frame = adjustedTextFrame
|
|
||||||
if let statusSizeAndApply = statusSizeAndApply, let statusFrame = statusFrame {
|
|
||||||
if strongSelf.statusNode.supernode == nil {
|
|
||||||
strongSelf.addSubnode(strongSelf.statusNode)
|
|
||||||
strongSelf.statusNode.frame = statusFrame
|
|
||||||
statusSizeAndApply.1(.None)
|
|
||||||
} else {
|
|
||||||
animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil)
|
|
||||||
statusSizeAndApply.1(animation)
|
|
||||||
}
|
|
||||||
} else if strongSelf.statusNode.supernode != nil {
|
|
||||||
strongSelf.statusNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let (size, apply) = actionButtonSizeAndApply {
|
|
||||||
let buttonNode = apply()
|
|
||||||
|
|
||||||
let buttonFrame = CGRect(origin: CGPoint(x: 12.0, y: adjustedBoundingSize.height - insets.bottom - size.height), size: size)
|
|
||||||
if buttonNode !== strongSelf.buttonNode {
|
|
||||||
strongSelf.buttonNode?.removeFromSupernode()
|
|
||||||
strongSelf.buttonNode = buttonNode
|
|
||||||
buttonNode.isUserInteractionEnabled = false
|
|
||||||
strongSelf.addSubnode(buttonNode)
|
|
||||||
buttonNode.pressed = {
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.activateAction?()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buttonNode.frame = buttonFrame
|
|
||||||
} else {
|
|
||||||
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
let buttonSeparatorFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + 8.0, y: buttonFrame.minY - 2.0), size: CGSize(width: buttonFrame.width - 8.0 - 8.0, height: UIScreenPixel))
|
|
||||||
|
|
||||||
let buttonSeparatorLayer: SimpleLayer
|
|
||||||
if let current = strongSelf.buttonSeparatorLayer {
|
|
||||||
buttonSeparatorLayer = current
|
|
||||||
animation.animator.updateFrame(layer: buttonSeparatorLayer, frame: buttonSeparatorFrame, completion: nil)
|
|
||||||
} else {
|
|
||||||
buttonSeparatorLayer = SimpleLayer()
|
|
||||||
strongSelf.buttonSeparatorLayer = buttonSeparatorLayer
|
|
||||||
strongSelf.layer.addSublayer(buttonSeparatorLayer)
|
|
||||||
buttonSeparatorLayer.frame = buttonSeparatorFrame
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonSeparatorLayer.backgroundColor = mainColor.withMultipliedAlpha(0.5).cgColor
|
|
||||||
} else {
|
|
||||||
if let buttonNode = strongSelf.buttonNode {
|
|
||||||
strongSelf.buttonNode = nil
|
|
||||||
buttonNode.removeFromSupernode()
|
|
||||||
}
|
|
||||||
|
|
||||||
if let buttonSeparatorLayer = strongSelf.buttonSeparatorLayer {
|
|
||||||
strongSelf.buttonSeparatorLayer = nil
|
|
||||||
buttonSeparatorLayer.removeFromSuperlayer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +79,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageWallpaperBubbleContentNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode",
|
||||||
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
|
"//submodules/TelegramUI/Components/Chat/ChatMessageGiveawayBubbleContentNode",
|
||||||
|
"//submodules/TelegramUI/Components/Chat/ChatMessageJoinedChannelBubbleContentNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -535,9 +535,16 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
private var swipeToReplyFeedback: HapticFeedback?
|
private var swipeToReplyFeedback: HapticFeedback?
|
||||||
|
|
||||||
private var nameNode: TextNode?
|
private var nameNode: TextNode?
|
||||||
|
private var nameButtonNode: HighlightTrackingButtonNode?
|
||||||
|
private var nameHighlightNode: ASImageNode?
|
||||||
|
|
||||||
private var adminBadgeNode: TextNode?
|
private var adminBadgeNode: TextNode?
|
||||||
private var credibilityIconView: ComponentHostView<Empty>?
|
private var credibilityIconView: ComponentHostView<Empty>?
|
||||||
private var credibilityIconComponent: EmojiStatusComponent?
|
private var credibilityIconComponent: EmojiStatusComponent?
|
||||||
|
private var credibilityIconContent: EmojiStatusComponent.Content?
|
||||||
|
private var credibilityButtonNode: HighlightTrackingButtonNode?
|
||||||
|
private var credibilityHighlightNode: ASImageNode?
|
||||||
|
|
||||||
private var closeButtonNode: HighlightTrackingButtonNode?
|
private var closeButtonNode: HighlightTrackingButtonNode?
|
||||||
private var closeIconNode: ASImageNode?
|
private var closeIconNode: ASImageNode?
|
||||||
|
|
||||||
@ -1064,7 +1071,15 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
return .fail
|
return .fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let nameButtonNode = strongSelf.nameButtonNode, nameButtonNode.frame.contains(point) {
|
||||||
|
return .fail
|
||||||
|
}
|
||||||
|
|
||||||
|
if let credibilityButtonNode = strongSelf.credibilityButtonNode, credibilityButtonNode.frame.contains(point) {
|
||||||
|
return .fail
|
||||||
|
}
|
||||||
|
|
||||||
if let nameNode = strongSelf.nameNode, nameNode.frame.contains(point) {
|
if let nameNode = strongSelf.nameNode, nameNode.frame.contains(point) {
|
||||||
if let item = strongSelf.item {
|
if let item = strongSelf.item {
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
@ -1540,6 +1555,10 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
var hasInstantVideo = false
|
var hasInstantVideo = false
|
||||||
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
for contentNodeItemValue in contentNodeMessagesAndClasses {
|
||||||
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
let contentNodeItem = contentNodeItemValue as (message: Message, type: AnyClass, attributes: ChatMessageEntryAttributes, bubbleAttributes: BubbleItemAttributes)
|
||||||
|
if contentNodeItem.type == ChatMessageGiveawayBubbleContentNode.self {
|
||||||
|
maximumContentWidth = 260.0
|
||||||
|
break
|
||||||
|
}
|
||||||
if contentNodeItem.type == ChatMessageInstantVideoBubbleContentNode.self, !contentNodeItem.bubbleAttributes.isAttachment {
|
if contentNodeItem.type == ChatMessageInstantVideoBubbleContentNode.self, !contentNodeItem.bubbleAttributes.isAttachment {
|
||||||
maximumContentWidth = baseWidth - 20.0
|
maximumContentWidth = baseWidth - 20.0
|
||||||
hasInstantVideo = true
|
hasInstantVideo = true
|
||||||
@ -2825,6 +2844,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let themeUpdated = strongSelf.appliedItem?.presentationData.theme.theme !== item.presentationData.theme.theme
|
||||||
let previousContextFrame = strongSelf.mainContainerNode.frame
|
let previousContextFrame = strongSelf.mainContainerNode.frame
|
||||||
strongSelf.mainContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
strongSelf.mainContainerNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||||
strongSelf.mainContextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
strongSelf.mainContextSourceNode.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||||
@ -2939,6 +2959,44 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil)
|
animation.animator.updateFrame(layer: nameNode.layer, frame: nameNodeFrame, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nameButtonNode: HighlightTrackingButtonNode
|
||||||
|
let nameHighlightNode: ASImageNode
|
||||||
|
if let currentButton = strongSelf.nameButtonNode, let currentHighlight = strongSelf.nameHighlightNode {
|
||||||
|
nameButtonNode = currentButton
|
||||||
|
nameHighlightNode = currentHighlight
|
||||||
|
} else {
|
||||||
|
nameHighlightNode = ASImageNode()
|
||||||
|
nameHighlightNode.alpha = 0.0
|
||||||
|
nameHighlightNode.displaysAsynchronously = false
|
||||||
|
nameHighlightNode.isUserInteractionEnabled = false
|
||||||
|
strongSelf.clippingNode.addSubnode(nameHighlightNode)
|
||||||
|
strongSelf.nameHighlightNode = nameHighlightNode
|
||||||
|
|
||||||
|
nameButtonNode = HighlightTrackingButtonNode()
|
||||||
|
nameButtonNode.highligthedChanged = { [weak nameHighlightNode] highlighted in
|
||||||
|
guard let nameHighlightNode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if highlighted {
|
||||||
|
nameHighlightNode.layer.removeAnimation(forKey: "opacity")
|
||||||
|
nameHighlightNode.alpha = 1.0
|
||||||
|
} else {
|
||||||
|
nameHighlightNode.alpha = 0.0
|
||||||
|
nameHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nameButtonNode.addTarget(strongSelf, action: #selector(strongSelf.nameButtonPressed), forControlEvents: .touchUpInside)
|
||||||
|
strongSelf.clippingNode.addSubnode(nameButtonNode)
|
||||||
|
strongSelf.nameButtonNode = nameButtonNode
|
||||||
|
}
|
||||||
|
nameHighlightNode.frame = nameNodeFrame.insetBy(dx: -2.0, dy: -1.0)
|
||||||
|
nameButtonNode.frame = nameNodeFrame.insetBy(dx: -2.0, dy: -3.0)
|
||||||
|
|
||||||
|
let nameColor = authorNameColor ?? item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||||
|
if themeUpdated {
|
||||||
|
nameHighlightNode.image = generateFilledRoundedRectImage(size: CGSize(width: 8.0, height: 8.0), cornerRadius: 4.0, color: nameColor.withAlphaComponent(0.1))?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4)
|
||||||
|
}
|
||||||
|
|
||||||
if let currentCredibilityIcon = currentCredibilityIcon {
|
if let currentCredibilityIcon = currentCredibilityIcon {
|
||||||
let credibilityIconView: ComponentHostView<Empty>
|
let credibilityIconView: ComponentHostView<Empty>
|
||||||
if let current = strongSelf.credibilityIconView {
|
if let current = strongSelf.credibilityIconView {
|
||||||
@ -2963,6 +3021,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
action: nil
|
action: nil
|
||||||
)
|
)
|
||||||
strongSelf.credibilityIconComponent = credibilityIconComponent
|
strongSelf.credibilityIconComponent = credibilityIconComponent
|
||||||
|
strongSelf.credibilityIconContent = currentCredibilityIcon
|
||||||
|
|
||||||
let credibilityIconSize = credibilityIconView.update(
|
let credibilityIconSize = credibilityIconView.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
@ -2971,10 +3030,49 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
containerSize: CGSize(width: 20.0, height: 20.0)
|
containerSize: CGSize(width: 20.0, height: 20.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
credibilityIconView.frame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 3.0, y: nameNode.frame.minY + floor((nameNode.bounds.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)
|
let credibilityIconFrame = CGRect(origin: CGPoint(x: nameNode.frame.maxX + 3.0, y: nameNode.frame.minY + floor((nameNode.bounds.height - credibilityIconSize.height) / 2.0)), size: credibilityIconSize)
|
||||||
|
credibilityIconView.frame = credibilityIconFrame
|
||||||
|
|
||||||
|
let credibilityButtonNode: HighlightTrackingButtonNode
|
||||||
|
let credibilityHighlightNode: ASImageNode
|
||||||
|
if let currentButton = strongSelf.credibilityButtonNode, let currentHighlight = strongSelf.credibilityHighlightNode {
|
||||||
|
credibilityButtonNode = currentButton
|
||||||
|
credibilityHighlightNode = currentHighlight
|
||||||
|
} else {
|
||||||
|
credibilityHighlightNode = ASImageNode()
|
||||||
|
credibilityHighlightNode.alpha = 0.0
|
||||||
|
credibilityHighlightNode.displaysAsynchronously = false
|
||||||
|
credibilityHighlightNode.isUserInteractionEnabled = false
|
||||||
|
strongSelf.clippingNode.addSubnode(credibilityHighlightNode)
|
||||||
|
strongSelf.credibilityHighlightNode = credibilityHighlightNode
|
||||||
|
|
||||||
|
credibilityButtonNode = HighlightTrackingButtonNode()
|
||||||
|
credibilityButtonNode.highligthedChanged = { [weak credibilityHighlightNode] highlighted in
|
||||||
|
guard let credibilityHighlightNode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if highlighted {
|
||||||
|
credibilityHighlightNode.layer.removeAnimation(forKey: "opacity")
|
||||||
|
credibilityHighlightNode.alpha = 1.0
|
||||||
|
} else {
|
||||||
|
credibilityHighlightNode.alpha = 0.0
|
||||||
|
credibilityHighlightNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
credibilityButtonNode.addTarget(strongSelf, action: #selector(strongSelf.credibilityButtonPressed), forControlEvents: .touchUpInside)
|
||||||
|
strongSelf.clippingNode.addSubnode(credibilityButtonNode)
|
||||||
|
strongSelf.credibilityButtonNode = credibilityButtonNode
|
||||||
|
}
|
||||||
|
credibilityHighlightNode.frame = credibilityIconFrame.insetBy(dx: -1.0, dy: -1.0)
|
||||||
|
credibilityButtonNode.frame = credibilityIconFrame.insetBy(dx: -2.0, dy: -3.0)
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
credibilityHighlightNode.image = generateFilledRoundedRectImage(size: CGSize(width: 8.0, height: 8.0), cornerRadius: 4.0, color: nameColor.withAlphaComponent(0.1))?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
strongSelf.credibilityIconView?.removeFromSuperview()
|
strongSelf.credibilityIconView?.removeFromSuperview()
|
||||||
strongSelf.credibilityIconView = nil
|
strongSelf.credibilityIconView = nil
|
||||||
|
strongSelf.credibilityIconContent = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let adminBadgeNode = adminNodeSizeApply.1() {
|
if let adminBadgeNode = adminNodeSizeApply.1() {
|
||||||
@ -3074,6 +3172,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
strongSelf.adminBadgeNode = nil
|
strongSelf.adminBadgeNode = nil
|
||||||
strongSelf.credibilityIconView?.removeFromSuperview()
|
strongSelf.credibilityIconView?.removeFromSuperview()
|
||||||
strongSelf.credibilityIconView = nil
|
strongSelf.credibilityIconView = nil
|
||||||
|
strongSelf.nameButtonNode?.removeFromSupernode()
|
||||||
|
strongSelf.nameButtonNode = nil
|
||||||
|
strongSelf.nameHighlightNode?.removeFromSupernode()
|
||||||
|
strongSelf.nameHighlightNode = nil
|
||||||
|
strongSelf.credibilityButtonNode?.removeFromSupernode()
|
||||||
|
strongSelf.credibilityButtonNode = nil
|
||||||
|
strongSelf.credibilityHighlightNode?.removeFromSupernode()
|
||||||
|
strongSelf.credibilityHighlightNode = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4420,6 +4526,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let nameButtonNode = self.nameButtonNode, nameButtonNode.frame.contains(point) {
|
||||||
|
return nameButtonNode.view
|
||||||
|
}
|
||||||
|
|
||||||
|
if let credibilityButtonNode = self.credibilityButtonNode, credibilityButtonNode.frame.contains(point) {
|
||||||
|
return credibilityButtonNode.view
|
||||||
|
}
|
||||||
|
|
||||||
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
|
if let shareButtonNode = self.shareButtonNode, shareButtonNode.frame.contains(point) {
|
||||||
return shareButtonNode.view
|
return shareButtonNode.view
|
||||||
}
|
}
|
||||||
@ -4827,6 +4941,28 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func nameButtonPressed() {
|
||||||
|
if let item = self.item, let peer = item.message.author {
|
||||||
|
let messageReference = MessageReference(item.message)
|
||||||
|
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
|
||||||
|
item.controllerInteraction.openPeer(EnginePeer(peer), .chat(textInputState: nil, subject: nil, peekData: nil), messageReference, .default)
|
||||||
|
} else {
|
||||||
|
item.controllerInteraction.openPeer(EnginePeer(peer), .info, messageReference, .groupParticipant(storyStats: nil, avatarHeaderNode: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func credibilityButtonPressed() {
|
||||||
|
if let item = self.item, let credibilityIconView = self.credibilityIconView, let iconContent = self.credibilityIconContent, let peer = item.message.author {
|
||||||
|
var emojiFileId: Int64?
|
||||||
|
if case let .animation(content, _, _, _, _) = iconContent {
|
||||||
|
emojiFileId = content.fileId.id
|
||||||
|
}
|
||||||
|
|
||||||
|
item.controllerInteraction.openPremiumStatusInfo(peer.id, credibilityIconView, emojiFileId, peer.nameColor ?? .blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var playedSwipeToReplyHaptic = false
|
private var playedSwipeToReplyHaptic = false
|
||||||
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
|
@objc private func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) {
|
||||||
var offset: CGFloat = 0.0
|
var offset: CGFloat = 0.0
|
||||||
|
@ -28,7 +28,6 @@ private func attributedServiceMessageString(theme: ChatPresentationThemeData, st
|
|||||||
public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
||||||
private let labelNode: TextNode
|
private let labelNode: TextNode
|
||||||
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
private var backgroundNode: WallpaperBubbleBackgroundNode?
|
||||||
private var backgroundColorNode: ASDisplayNode
|
|
||||||
private let backgroundMaskNode: ASImageNode
|
private let backgroundMaskNode: ASImageNode
|
||||||
private var linkHighlightingNode: LinkHighlightingNode?
|
private var linkHighlightingNode: LinkHighlightingNode?
|
||||||
|
|
||||||
@ -78,7 +77,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
self.labelNode.isUserInteractionEnabled = false
|
self.labelNode.isUserInteractionEnabled = false
|
||||||
self.labelNode.displaysAsynchronously = false
|
self.labelNode.displaysAsynchronously = false
|
||||||
|
|
||||||
self.backgroundColorNode = ASDisplayNode()
|
|
||||||
self.backgroundMaskNode = ASImageNode()
|
self.backgroundMaskNode = ASImageNode()
|
||||||
|
|
||||||
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
|
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
|
||||||
@ -367,9 +365,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
strongSelf.updateVisibility()
|
strongSelf.updateVisibility()
|
||||||
|
|
||||||
strongSelf.labelNode.isHidden = !hasServiceMessage
|
strongSelf.labelNode.isHidden = !hasServiceMessage
|
||||||
|
|
||||||
strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)
|
|
||||||
|
|
||||||
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize)
|
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - giftSize.width) / 2.0), y: hasServiceMessage ? labelLayout.size.height + 16.0 : 0.0), size: giftSize)
|
||||||
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
|
let mediaBackgroundFrame = imageFrame.insetBy(dx: -2.0, dy: -2.0)
|
||||||
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
|
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
|
||||||
@ -426,7 +422,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
if strongSelf.backgroundNode == nil {
|
if strongSelf.backgroundNode == nil {
|
||||||
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
if let backgroundNode = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
|
||||||
strongSelf.backgroundNode = backgroundNode
|
strongSelf.backgroundNode = backgroundNode
|
||||||
backgroundNode.addSubnode(strongSelf.backgroundColorNode)
|
|
||||||
strongSelf.insertSubnode(backgroundNode, at: 0)
|
strongSelf.insertSubnode(backgroundNode, at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -448,7 +443,6 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
strongSelf.backgroundMaskNode.image = image
|
strongSelf.backgroundMaskNode.image = image
|
||||||
strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size)
|
strongSelf.backgroundMaskNode.frame = CGRect(origin: CGPoint(), size: image.size)
|
||||||
strongSelf.backgroundColorNode.frame = CGRect(origin: CGPoint(), size: image.size)
|
|
||||||
|
|
||||||
strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects)
|
strongSelf.cachedMaskBackgroundImage = (offset, image, labelRects)
|
||||||
}
|
}
|
||||||
|
@ -218,13 +218,17 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
|
|||||||
let backgroundColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first!
|
let backgroundColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first!
|
||||||
let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
|
let textColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.primaryTextColor : item.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
|
||||||
let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
let accentColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.accentTextColor : item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||||
|
var badgeTextColor: UIColor = .white
|
||||||
|
if badgeTextColor.distance(to: accentColor) < 1 {
|
||||||
|
badgeTextColor = incoming ? item.presentationData.theme.theme.chat.message.incoming.bubble.withoutWallpaper.fill.first! : item.presentationData.theme.theme.chat.message.outgoing.bubble.withoutWallpaper.fill.first!
|
||||||
|
}
|
||||||
|
|
||||||
var updatedBadgeImage: UIImage?
|
var updatedBadgeImage: UIImage?
|
||||||
if themeUpdated {
|
if themeUpdated {
|
||||||
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)
|
updatedBadgeImage = generateStretchableFilledCircleImage(diameter: 21.0, color: accentColor, strokeColor: backgroundColor, strokeWidth: 1.0 + UIScreenPixel, backgroundColor: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: .white)
|
let badgeString = NSAttributedString(string: "X\(giveaway?.quantity ?? 1)", font: Font.with(size: 10.0, design: .round , weight: .bold, traits: .monospacedNumbers), textColor: badgeTextColor)
|
||||||
|
|
||||||
let prizeTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor)
|
let prizeTitleString = NSAttributedString(string: item.presentationData.strings.Chat_Giveaway_Message_PrizeTitle, font: titleFont, textColor: textColor)
|
||||||
var prizeTextString: NSAttributedString?
|
var prizeTextString: NSAttributedString?
|
||||||
@ -433,7 +437,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 240.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1))
|
let (channelsWidth, continueChannelLayout) = makeChannelsLayout(item.context, 220.0, channelPeers, accentColor, accentColor.withAlphaComponent(0.1))
|
||||||
maxContentWidth = max(maxContentWidth, channelsWidth)
|
maxContentWidth = max(maxContentWidth, channelsWidth)
|
||||||
maxContentWidth += 30.0
|
maxContentWidth += 30.0
|
||||||
|
|
||||||
|
@ -569,6 +569,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|
|||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
}, displayGiveawayParticipationStatus: { _ in
|
}, displayGiveawayParticipationStatus: { _ in
|
||||||
|
}, openPremiumStatusInfo: { _, _, _, _ in
|
||||||
}, requestMessageUpdate: { _, _ in
|
}, requestMessageUpdate: { _, _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, dismissTextInput: {
|
}, dismissTextInput: {
|
||||||
|
@ -209,6 +209,7 @@ public final class ChatControllerInteraction {
|
|||||||
public let saveMediaToFiles: (EngineMessage.Id) -> Void
|
public let saveMediaToFiles: (EngineMessage.Id) -> Void
|
||||||
public let openNoAdsDemo: () -> Void
|
public let openNoAdsDemo: () -> Void
|
||||||
public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void
|
public let displayGiveawayParticipationStatus: (EngineMessage.Id) -> Void
|
||||||
|
public let openPremiumStatusInfo: (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void
|
||||||
|
|
||||||
public let requestMessageUpdate: (MessageId, Bool) -> Void
|
public let requestMessageUpdate: (MessageId, Bool) -> Void
|
||||||
public let cancelInteractiveKeyboardGestures: () -> Void
|
public let cancelInteractiveKeyboardGestures: () -> Void
|
||||||
@ -327,6 +328,7 @@ public final class ChatControllerInteraction {
|
|||||||
saveMediaToFiles: @escaping (EngineMessage.Id) -> Void,
|
saveMediaToFiles: @escaping (EngineMessage.Id) -> Void,
|
||||||
openNoAdsDemo: @escaping () -> Void,
|
openNoAdsDemo: @escaping () -> Void,
|
||||||
displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void,
|
displayGiveawayParticipationStatus: @escaping (EngineMessage.Id) -> Void,
|
||||||
|
openPremiumStatusInfo: @escaping (EnginePeer.Id, UIView, Int64?, PeerNameColor) -> Void,
|
||||||
requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
|
requestMessageUpdate: @escaping (MessageId, Bool) -> Void,
|
||||||
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
cancelInteractiveKeyboardGestures: @escaping () -> Void,
|
||||||
dismissTextInput: @escaping () -> Void,
|
dismissTextInput: @escaping () -> Void,
|
||||||
@ -427,6 +429,7 @@ public final class ChatControllerInteraction {
|
|||||||
self.saveMediaToFiles = saveMediaToFiles
|
self.saveMediaToFiles = saveMediaToFiles
|
||||||
self.openNoAdsDemo = openNoAdsDemo
|
self.openNoAdsDemo = openNoAdsDemo
|
||||||
self.displayGiveawayParticipationStatus = displayGiveawayParticipationStatus
|
self.displayGiveawayParticipationStatus = displayGiveawayParticipationStatus
|
||||||
|
self.openPremiumStatusInfo = openPremiumStatusInfo
|
||||||
self.requestMessageUpdate = requestMessageUpdate
|
self.requestMessageUpdate = requestMessageUpdate
|
||||||
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
|
||||||
self.dismissTextInput = dismissTextInput
|
self.dismissTextInput = dismissTextInput
|
||||||
|
@ -23,7 +23,7 @@ public final class EmojiStatusComponent: Component {
|
|||||||
case file(file: TelegramMediaFile)
|
case file(file: TelegramMediaFile)
|
||||||
case customEmoji(fileId: Int64)
|
case customEmoji(fileId: Int64)
|
||||||
|
|
||||||
var fileId: MediaId {
|
public var fileId: MediaId {
|
||||||
switch self {
|
switch self {
|
||||||
case let .file(file):
|
case let .file(file):
|
||||||
return file.fileId
|
return file.fileId
|
||||||
|
@ -886,6 +886,11 @@ private extension MediaEditorValues {
|
|||||||
if let paintingData = legacyAdjustments.paintingData {
|
if let paintingData = legacyAdjustments.paintingData {
|
||||||
if let entitiesData = paintingData.entitiesData {
|
if let entitiesData = paintingData.entitiesData {
|
||||||
entities = decodeCodableDrawingEntities(data: entitiesData)
|
entities = decodeCodableDrawingEntities(data: entitiesData)
|
||||||
|
|
||||||
|
let hasAnimation = entities.first(where: { $0.entity.isAnimated }) != nil
|
||||||
|
if !hasAnimation {
|
||||||
|
entities = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let imagePath = paintingData.imagePath, let image = UIImage(contentsOfFile: imagePath) {
|
if let imagePath = paintingData.imagePath, let image = UIImage(contentsOfFile: imagePath) {
|
||||||
drawing = image
|
drawing = image
|
||||||
|
@ -233,7 +233,7 @@ private func peerNameColorScreenEntries(
|
|||||||
}
|
}
|
||||||
let messageItem = PeerNameColorChatPreviewItem.MessageItem(
|
let messageItem = PeerNameColorChatPreviewItem.MessageItem(
|
||||||
outgoing: false,
|
outgoing: false,
|
||||||
peerId: peer.id,
|
peerId: PeerId(namespace: peer.id.namespace, id: PeerId.Id._internalFromInt64Value(0)),
|
||||||
author: peer.compactDisplayTitle,
|
author: peer.compactDisplayTitle,
|
||||||
photo: peer.profileImageRepresentations,
|
photo: peer.profileImageRepresentations,
|
||||||
nameColor: nameColor,
|
nameColor: nameColor,
|
||||||
|
@ -829,9 +829,9 @@ final class CountriesMultiselectionScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset
|
navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset
|
||||||
|
|
||||||
let actionButtonTitle = "Save Countries"
|
let actionButtonTitle = environment.strings.CountriesList_SaveCountries
|
||||||
let title = "Select Countries"
|
let title = environment.strings.CountriesList_SelectCountries
|
||||||
let subtitle = "select up to \(component.context.userLimits.maxGiveawayCountriesCount) countries"
|
let subtitle = environment.strings.CountriesList_SelectUpTo(component.context.userLimits.maxGiveawayCountriesCount)
|
||||||
|
|
||||||
let titleComponent = AnyComponent<Empty>(
|
let titleComponent = AnyComponent<Empty>(
|
||||||
List([
|
List([
|
||||||
|
@ -588,17 +588,22 @@ public extension ShareWithPeersScreen {
|
|||||||
case let .channels(excludePeerIds, searchQuery):
|
case let .channels(excludePeerIds, searchQuery):
|
||||||
self.stateDisposable = (combineLatest(
|
self.stateDisposable = (combineLatest(
|
||||||
context.engine.messages.chatList(group: .root, count: 500) |> take(1),
|
context.engine.messages.chatList(group: .root, count: 500) |> take(1),
|
||||||
|
searchQuery.flatMap { context.engine.contacts.searchLocalPeers(query: $0) } ?? .single([]),
|
||||||
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init)))
|
context.engine.data.get(EngineDataMap(Array(self.initialPeerIds).map(TelegramEngine.EngineData.Item.Peer.Peer.init)))
|
||||||
)
|
)
|
||||||
|> mapToSignal { chatList, initialPeers -> Signal<(EngineChatList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]), NoError> in
|
|> mapToSignal { chatList, searchResults, initialPeers -> Signal<(EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]), NoError> in
|
||||||
|
var peerIds: [EnginePeer.Id] = []
|
||||||
|
peerIds.append(contentsOf: chatList.items.map(\.renderedPeer.peerId))
|
||||||
|
peerIds.append(contentsOf: searchResults.map(\.peerId))
|
||||||
|
peerIds.append(contentsOf: initialPeers.compactMap(\.value?.id))
|
||||||
return context.engine.data.subscribe(
|
return context.engine.data.subscribe(
|
||||||
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
||||||
)
|
)
|
||||||
|> map { participantCountMap -> (EngineChatList, [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]) in
|
|> map { participantCountMap -> (EngineChatList, [EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer>], [EnginePeer.Id: Optional<Int>]) in
|
||||||
return (chatList, initialPeers, participantCountMap)
|
return (chatList, searchResults, initialPeers, participantCountMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] chatList, initialPeers, participantCounts in
|
|> deliverOnMainQueue).start(next: { [weak self] chatList, searchResults, initialPeers, participantCounts in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -612,7 +617,7 @@ public extension ShareWithPeersScreen {
|
|||||||
|
|
||||||
var existingIds = Set<EnginePeer.Id>()
|
var existingIds = Set<EnginePeer.Id>()
|
||||||
var selectedPeers: [EnginePeer] = []
|
var selectedPeers: [EnginePeer] = []
|
||||||
|
|
||||||
for item in chatList.items.reversed() {
|
for item in chatList.items.reversed() {
|
||||||
if let peer = item.renderedPeer.peer {
|
if let peer = item.renderedPeer.peer {
|
||||||
if self.initialPeerIds.contains(peer.id) {
|
if self.initialPeerIds.contains(peer.id) {
|
||||||
@ -628,6 +633,13 @@ public extension ShareWithPeersScreen {
|
|||||||
existingIds.insert(peerId)
|
existingIds.insert(peerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for item in searchResults {
|
||||||
|
if let peer = item.peer, case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||||
|
selectedPeers.append(peer)
|
||||||
|
existingIds.insert(peer.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let queryTokens = stringIndexTokens(searchQuery ?? "", transliteration: .combined)
|
let queryTokens = stringIndexTokens(searchQuery ?? "", transliteration: .combined)
|
||||||
func peerMatchesTokens(peer: EnginePeer, tokens: [ValueBoxKey]) -> Bool {
|
func peerMatchesTokens(peer: EnginePeer, tokens: [ValueBoxKey]) -> Bool {
|
||||||
@ -640,9 +652,15 @@ public extension ShareWithPeersScreen {
|
|||||||
var peers: [EnginePeer] = []
|
var peers: [EnginePeer] = []
|
||||||
peers = chatList.items.filter { peer in
|
peers = chatList.items.filter { peer in
|
||||||
if let peer = peer.renderedPeer.peer {
|
if let peer = peer.renderedPeer.peer {
|
||||||
|
if existingIds.contains(peer.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if excludePeerIds.contains(peer.id) {
|
if excludePeerIds.contains(peer.id) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if peer.isFake || peer.isScam {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if let _ = searchQuery, !peerMatchesTokens(peer: peer, tokens: queryTokens) {
|
if let _ = searchQuery, !peerMatchesTokens(peer: peer, tokens: queryTokens) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -538,7 +538,11 @@ private final class StoryContainerScreenComponent: Component {
|
|||||||
let fraction = translation.x / (self.bounds.width / 2.0)
|
let fraction = translation.x / (self.bounds.width / 2.0)
|
||||||
timestamp = initialSeekTimestamp + duration * fraction
|
timestamp = initialSeekTimestamp + duration * fraction
|
||||||
}
|
}
|
||||||
visibleItemView.seekTo(max(0.0, min(duration, timestamp)), apply: apply)
|
if translation.y < 64.0 {
|
||||||
|
visibleItemView.seekTo(max(0.0, min(duration, timestamp)), apply: apply)
|
||||||
|
} else {
|
||||||
|
visibleItemView.seekTo(initialSeekTimestamp, apply: apply)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
longPressRecognizer.updatePanEnded = { [weak self] in
|
longPressRecognizer.updatePanEnded = { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
|
@ -4852,6 +4852,65 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
self.present(controller, in: .current)
|
self.present(controller, in: .current)
|
||||||
|
|
||||||
}))
|
}))
|
||||||
|
}, openPremiumStatusInfo: { [weak self] peerId, sourceView, peerStatus, nameColor in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = self.context
|
||||||
|
let source: Signal<PremiumSource, NoError>
|
||||||
|
if let peerStatus {
|
||||||
|
source = context.engine.stickers.resolveInlineStickers(fileIds: [peerStatus])
|
||||||
|
|> mapToSignal { files in
|
||||||
|
if let file = files[peerStatus] {
|
||||||
|
var reference: StickerPackReference?
|
||||||
|
for attribute in file.attributes {
|
||||||
|
if case let .CustomEmoji(_, _, _, packReference) = attribute, let packReference = packReference {
|
||||||
|
reference = packReference
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let reference {
|
||||||
|
return context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false)
|
||||||
|
|> filter { result in
|
||||||
|
if case .result = result {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> take(1)
|
||||||
|
|> mapToSignal { result -> Signal<PremiumSource, NoError> in
|
||||||
|
if case let .result(_, items, _) = result {
|
||||||
|
return .single(.emojiStatus(peerId, peerStatus, items.first?.file, result))
|
||||||
|
} else {
|
||||||
|
return .single(.emojiStatus(peerId, peerStatus, nil, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .single(.emojiStatus(peerId, peerStatus, nil, nil))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .single(.emojiStatus(peerId, peerStatus, nil, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source = .single(.profile(peerId))
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (source
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] source in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let controller = PremiumIntroScreen(context: self.context, source: source)
|
||||||
|
controller.sourceView = sourceView
|
||||||
|
controller.containerView = self.navigationController?.view
|
||||||
|
controller.animationColor = self.context.peerNameColors.get(nameColor, dark: self.presentationData.theme.overallDarkAppearance).main
|
||||||
|
self.push(controller)
|
||||||
|
})
|
||||||
|
|
||||||
}, requestMessageUpdate: { [weak self] id, scroll in
|
}, requestMessageUpdate: { [weak self] id, scroll in
|
||||||
if let self {
|
if let self {
|
||||||
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
|
self.chatDisplayNode.historyNode.requestMessageUpdate(id, andScrollToItem: scroll)
|
||||||
|
@ -565,6 +565,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
var loadStickerSaveStatus: MediaId?
|
var loadStickerSaveStatus: MediaId?
|
||||||
var loadCopyMediaResource: MediaResource?
|
var loadCopyMediaResource: MediaResource?
|
||||||
var isAction = false
|
var isAction = false
|
||||||
|
var isGiveawayLaunch = false
|
||||||
var diceEmoji: String?
|
var diceEmoji: String?
|
||||||
if messages.count == 1 {
|
if messages.count == 1 {
|
||||||
for media in messages[0].media {
|
for media in messages[0].media {
|
||||||
@ -579,6 +580,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
}
|
}
|
||||||
} else if media is TelegramMediaAction || media is TelegramMediaExpiredContent {
|
} else if media is TelegramMediaAction || media is TelegramMediaExpiredContent {
|
||||||
isAction = true
|
isAction = true
|
||||||
|
if let action = media as? TelegramMediaAction, case .giveawayLaunched = action.action {
|
||||||
|
isGiveawayLaunch = true
|
||||||
|
}
|
||||||
} else if let image = media as? TelegramMediaImage {
|
} else if let image = media as? TelegramMediaImage {
|
||||||
if !messages[0].containsSecretMedia {
|
if !messages[0].containsSecretMedia {
|
||||||
loadCopyMediaResource = largestImageRepresentation(image.representations)?.resource
|
loadCopyMediaResource = largestImageRepresentation(image.representations)?.resource
|
||||||
@ -639,6 +643,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
canPin = false
|
canPin = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isGiveawayLaunch {
|
||||||
|
canReply = false
|
||||||
|
}
|
||||||
|
|
||||||
if let peer = messages[0].peers[messages[0].id.peerId] {
|
if let peer = messages[0].peers[messages[0].id.peerId] {
|
||||||
if peer.isDeleted {
|
if peer.isDeleted {
|
||||||
canPin = false
|
canPin = false
|
||||||
@ -925,7 +933,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
resourceAvailable = false
|
resourceAvailable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if !isPremium && isDownloading {
|
if !isPremium && isDownloading {
|
||||||
var isLargeFile = false
|
var isLargeFile = false
|
||||||
for media in message.media {
|
for media in message.media {
|
||||||
|
@ -170,6 +170,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
}, displayGiveawayParticipationStatus: { _ in
|
}, displayGiveawayParticipationStatus: { _ in
|
||||||
|
}, openPremiumStatusInfo: { _, _, _, _ in
|
||||||
}, requestMessageUpdate: { _, _ in
|
}, requestMessageUpdate: { _, _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, dismissTextInput: {
|
}, dismissTextInput: {
|
||||||
|
@ -2921,6 +2921,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
}, displayGiveawayParticipationStatus: { _ in
|
}, displayGiveawayParticipationStatus: { _ in
|
||||||
|
}, openPremiumStatusInfo: { _, _, _, _ in
|
||||||
}, requestMessageUpdate: { _, _ in
|
}, requestMessageUpdate: { _, _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, dismissTextInput: {
|
}, dismissTextInput: {
|
||||||
@ -3794,37 +3795,31 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId))
|
let source: Signal<PremiumSource, NoError>
|
||||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] _ in
|
if let peerStatus = peerStatus {
|
||||||
|
source = emojiStatusFileAndPack
|
||||||
|
|> take(1)
|
||||||
|
|> mapToSignal { emojiStatusFileAndPack -> Signal<PremiumSource, NoError> in
|
||||||
|
if let (file, pack) = emojiStatusFileAndPack {
|
||||||
|
return .single(.emojiStatus(strongSelf.peerId, peerStatus.fileId, file, pack))
|
||||||
|
} else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
source = .single(.profile(strongSelf.peerId))
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = (source
|
||||||
|
|> deliverOnMainQueue).startStandalone(next: { [weak self] source in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let source: Signal<PremiumSource, NoError>
|
let controller = PremiumIntroScreen(context: strongSelf.context, source: source)
|
||||||
if let peerStatus = peerStatus {
|
controller.sourceView = sourceView
|
||||||
source = emojiStatusFileAndPack
|
controller.containerView = strongSelf.controller?.navigationController?.view
|
||||||
|> take(1)
|
controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor
|
||||||
|> mapToSignal { emojiStatusFileAndPack -> Signal<PremiumSource, NoError> in
|
strongSelf.controller?.push(controller)
|
||||||
if let (file, pack) = emojiStatusFileAndPack {
|
|
||||||
return .single(.emojiStatus(strongSelf.peerId, peerStatus.fileId, file, pack))
|
|
||||||
} else {
|
|
||||||
return .complete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
source = .single(.profile(strongSelf.peerId))
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = (source
|
|
||||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] source in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let controller = PremiumIntroScreen(context: strongSelf.context, source: source)
|
|
||||||
controller.sourceView = sourceView
|
|
||||||
controller.containerView = strongSelf.controller?.navigationController?.view
|
|
||||||
controller.animationColor = white ? .white : strongSelf.presentationData.theme.list.itemAccentColor
|
|
||||||
strongSelf.controller?.push(controller)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1559,6 +1559,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
}, saveMediaToFiles: { _ in
|
}, saveMediaToFiles: { _ in
|
||||||
}, openNoAdsDemo: {
|
}, openNoAdsDemo: {
|
||||||
}, displayGiveawayParticipationStatus: { _ in
|
}, displayGiveawayParticipationStatus: { _ in
|
||||||
|
}, openPremiumStatusInfo: { _, _, _, _ in
|
||||||
}, requestMessageUpdate: { _, _ in
|
}, requestMessageUpdate: { _, _ in
|
||||||
}, cancelInteractiveKeyboardGestures: {
|
}, cancelInteractiveKeyboardGestures: {
|
||||||
}, dismissTextInput: {
|
}, dismissTextInput: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user