This commit is contained in:
Isaac 2025-02-27 12:23:18 +01:00
commit e5908c0bc8
72 changed files with 2763 additions and 679 deletions

View file

@ -13875,6 +13875,26 @@ Sorry for the inconvenience.";
"Chat.PanelCustomStatusShortInfo" = "%@ is a mark for [Premium subscribers >]()";
"Chat.NonContactUser.Subtitle" = "Not a contact";
"Chat.NonContactUser.PhoneNumber" = "Phone Number";
"Chat.NonContactUser.Registration" = "Registration";
"Chat.NonContactUser.Groups" = "Shared Groups";
"Chat.NonContactUser.GroupsCount_1" = "%@ group";
"Chat.NonContactUser.GroupsCount_any" = "%@ groups";
"Chat.NonContactUser.GroupsNone" = "None";
"Chat.NonContactUser.Disclaimer" = "Not an official account";
"Chat.NonContactUser.AnonymousNumber" = "Anonymous";
"Chat.NonContactUser.UpdatedName" = "User updated name %1$@";
"Chat.NonContactUser.UpdatedPhoto" = "User updated photo %1$@";
"Chat.NonContactUser.UpdatedMinutes_1" = "%@ minute ago";
"Chat.NonContactUser.UpdatedMinutes_any" = "%@ minutes ago";
"Chat.NonContactUser.UpdatedHours_1" = "%@ hour ago";
"Chat.NonContactUser.UpdatedHours_any" = "%@ hours ago";
"Chat.NonContactUser.UpdatedDays_1" = "%@ day ago";
"Chat.NonContactUser.UpdatedDays_any" = "%@ days ago";
"Chat.InputTextPaidMessagePlaceholder" = "Message for %@";
"Privacy.Messages.Stars_1" = "%@ Star";
@ -13884,3 +13904,72 @@ Sorry for the inconvenience.";
"Premium.PaidMessages" = "Paid Messages";
"Premium.PaidMessagesInfo" = "Charge a fee for messages from non-contacts or new senders.";
"Premium.PaidMessages.Proceed" = "About Telegram Premium";
"PeerInfo.Gifts.Reorder" = "Reorder";
"PeerInfo.Gifts.Context.Pin" = "Pin";
"PeerInfo.Gifts.Context.Unpin" = "Unpin";
"PeerInfo.Gifts.Context.Reorder" = "Reorder";
"PeerInfo.Gifts.Context.Wear" = "Wear";
"PeerInfo.Gifts.Context.CopyLink" = "Copy Link";
"PeerInfo.Gifts.Context.Share" = "Share";
"PeerInfo.Gifts.Context.Hide" = "Hide";
"PeerInfo.Gifts.Context.Show" = "Show";
"PeerInfo.Gifts.Context.Transfer" = "Transfer";
"Gift.Send.Premium.Confirmation.Title" = "Send a Gift";
"Gift.Send.Premium.Confirmation.Text" = "Are you sure you want to gift **Telegram Premium** to **%1$@** for **%2$@**?";
"Gift.Send.Premium.Confirmation.Text.Stars_1" = "%@ Star";
"Gift.Send.Premium.Confirmation.Text.Stars_any" = "%@ Stars";
"Gift.Send.Premium.Confirmation.Confirm" = "Confirm";
"StarsBalance.YourBalance" = "Your balance is %@";
"StarsBalance.GetMoreStars" = "Get More Stars >";
"Stars.Intro.BuyShort" = "Top Up";
"Stars.Intro.Withdraw" = "Withdraw";
"Group.Info.Settings" = "Group Settings";
"Monetization.Balance.StarsInfoGroup" = "You can collect rewards for Stars using Fragment. You cannot withdraw less than 1000 stars. [Learn More >]()";
"PeerInfo.Gifts.ToastPinLimit.Text_1" = "You can't pin more than %@ gift.";
"PeerInfo.Gifts.ToastPinLimit.Text_any" = "You can't pin more than %@ gifts.";
"PeerInfo.Gifts.ToastUnpinned.Text" = "Gift Unpinned.";
"PeerInfo.Gifts.ToastPinned.Title" = "Gift Pinned";
"PeerInfo.Gifts.ToastPinned.Text" = "Now it will always be shown on the top.";
"Chat.PaidMessage.Confirm.Title" = "Confirm Payment";
"Chat.PaidMessage.Confirm.DontAskAgain" = "Don't ask again";
"Chat.PaidMessage.Confirm.PayForMessage_1" = "Pay for %@ Message";
"Chat.PaidMessage.Confirm.PayForMessage_any" = "Pay for %@ Messages";
"Chat.PaidMessage.Confirm.Single.Text" = "**%1$@** charges **%2$@** per incoming message. Would you like to pay **%3$@** to send %4$@?";
"Chat.PaidMessage.Confirm.Multiple.Text" = "You selected %1$@ who charge Stars for messages. Would you like to pay **%2$@** Stars to send %3$@?";
"Chat.PaidMessage.Confirm.Text.Users_1" = "**%@** user";
"Chat.PaidMessage.Confirm.Text.Users_any" = "**%@** users";
"Chat.PaidMessage.Confirm.Text.Messages_1" = "**%@** message";
"Chat.PaidMessage.Confirm.Text.Messages_any" = "**%@** messages";
"Chat.PaidMessage.Confirm.Text.Stars_1" = "%@ Star";
"Chat.PaidMessage.Confirm.Text.Stars_any" = "%@ Stars";
"Chat.PaidMessage.RemoveFee.Title" = "Remove Fee";
"Chat.PaidMessage.RemoveFee.Text" = "Are you sure you want to allow **%@** to message you for free?";
"Chat.PaidMessage.RemoveFee.Yes" = "Yes";
"Chat.PaidMessage.RemoveFee.Refund" = "Refund already paid **%@**";
"Chat.PaidMessage.RemoveFee.Refund.Stars_1" = "%@ Star";
"Chat.PaidMessage.RemoveFee.Refund.Stars_any" = "%@ Stars";
"Chat.PaidMessage.Sent.Title_1" = "Message Sent";
"Chat.PaidMessage.Sent.Title_any" = "%@ Messages Sent";
"Chat.PaidMessage.Sent.Text" = "You paid %@";
"Chat.PaidMessage.Sent.Text.Stars_1" = "%@ Star";
"Chat.PaidMessage.Sent.Text.Stars_any" = "%@ Stars";
"Chat.PaidMessageFee.Text" = "%1$@ must pay %2$@ for each message to you.";
"Chat.PaidMessageFee.RemoveFee" = "Remove Fee";
"Privacy.Review.Review" = "Review";
"Privacy.Review.Calls.Title" = "Call Settings";
"Privacy.Review.Calls.Text" = "You've restricted who can message you, but anyone can still call you. Would you like to review these settings?";
"Privacy.Review.Invite.Title" = "Invitation Settings";
"Privacy.Review.Invite.Text" = "You've restricted who can message you, but anyone can still invite you to groups and channels. Would you like to review these settings?";

View file

@ -593,6 +593,7 @@ public enum PeerInfoControllerMode {
case myProfile
case gifts
case myProfileGifts
case groupsInCommon
}
public enum ContactListActionItemInlineIconPosition {
@ -1100,6 +1101,7 @@ public protocol SharedAccountContext: AnyObject {
func makeStarsStatisticsScreen(context: AccountContext, peerId: EnginePeer.Id, revenueContext: StarsRevenueStatsContext) -> ViewController
func makeStarsAmountScreen(context: AccountContext, initialValue: Int64?, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, stats: StarsRevenueStats, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController
func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController
func makeStarsGiveawayBoostScreen(context: AccountContext, peerId: EnginePeer.Id, boost: ChannelBoostersContext.State.Boost) -> ViewController
func makeStarsIntroScreen(context: AccountContext) -> ViewController

View file

@ -314,10 +314,12 @@ public struct ChatControllerInitialBotAppStart {
public enum ChatControllerInteractionNavigateToPeer {
public struct InfoParams {
public let switchToRecommendedChannels: Bool
public let switchToGroupsInCommon: Bool
public let ignoreInSavedMessages: Bool
public init(switchToRecommendedChannels: Bool = false, ignoreInSavedMessages: Bool = false) {
public init(switchToRecommendedChannels: Bool = false, switchToGroupsInCommon: Bool = false, ignoreInSavedMessages: Bool = false) {
self.switchToRecommendedChannels = switchToRecommendedChannels
self.switchToGroupsInCommon = switchToGroupsInCommon
self.ignoreInSavedMessages = ignoreInSavedMessages
}
}

View file

@ -908,7 +908,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
self.textPlaceholderNode.isHidden = inputHasText
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
let additionalRightInset = self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: transition)
let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right - additionalRightInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom)
transition.updateFrame(node: self.textInputContainer, frame: textInputFrame)
if let textInputNode = self.textInputNode {
@ -923,16 +925,14 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
}
}
self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: transition)
self.actionButtons.updateAccessibility()
return panelHeight
}
private func updateFieldAndButtonsLayout(inputHasText: Bool, panelHeight: CGFloat, transition: ContainedViewLayoutTransition) {
private func updateFieldAndButtonsLayout(inputHasText: Bool, panelHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
guard let (width, leftInset, rightInset, additionalSideInsets, _, metrics, _) = self.validLayout else {
return
return 0.0
}
var textFieldMinHeight: CGFloat = 33.0
if let presentationInterfaceState = self.presentationInterfaceState {
@ -954,6 +954,11 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let baseWidth = width - leftInset - rightInset
let textInputFrame = self.textInputContainer.frame
var textFieldInsets = self.textFieldInsets(metrics: metrics)
if additionalSideInsets.right > 0.0 {
textFieldInsets.right += additionalSideInsets.right / 3.0
}
var textBackgroundInset: CGFloat = 0.0
let actionButtonsSize: CGSize
if let presentationInterfaceState = self.presentationInterfaceState {
@ -968,24 +973,19 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
text = presentationInterfaceState.strings.MediaPicker_Send
}
actionButtonsSize = self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, minimized: isMinimized, text: text, interfaceState: presentationInterfaceState)
textBackgroundInset = 44.0 - actionButtonsSize.width
textBackgroundInset = actionButtonsSize.width - 44.0
} else {
actionButtonsSize = CGSize(width: 44.0, height: minimalHeight)
}
var textFieldInsets = self.textFieldInsets(metrics: metrics)
if additionalSideInsets.right > 0.0 {
textFieldInsets.right += additionalSideInsets.right / 3.0
}
let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - actionButtonsSize.width + 1.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: actionButtonsSize)
transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame)
let textInputBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: textInputFrame.size.width + composeButtonsOffset + textBackgroundInset, height: textInputFrame.size.height))
let textInputBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: textInputFrame.size.width + composeButtonsOffset, height: textInputFrame.size.height))
transition.updateFrame(node: self.textInputContainerBackgroundNode, frame: textInputBackgroundFrame)
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset + textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
transition.updateFrame(layer: self.textInputBackgroundImageNode.layer, frame: CGRect(x: 0.0, y: 0.0, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset + textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
transition.updateFrame(layer: self.textInputBackgroundNode.layer, frame: CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
transition.updateFrame(layer: self.textInputBackgroundImageNode.layer, frame: CGRect(x: 0.0, y: 0.0, width: baseWidth - textFieldInsets.left - textFieldInsets.right + composeButtonsOffset - textBackgroundInset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom))
var textInputViewRealInsets = UIEdgeInsets()
if let presentationInterfaceState = self.presentationInterfaceState {
@ -1027,6 +1027,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
placeholderFrame = CGRect(origin: CGPoint(x: leftInset + textFieldInsets.left + self.textInputViewInternalInsets.left, y: textFieldInsets.top + self.textInputViewInternalInsets.top + textInputViewRealInsets.top + UIScreenPixel), size: self.textPlaceholderNode.frame.size)
}
transition.updateFrame(node: self.textPlaceholderNode, frame: placeholderFrame)
return textBackgroundInset
}
private var skipUpdate = false
@ -1387,7 +1389,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
let panelHeight = self.updateTextHeight(animated: animated)
if self.isAttachment, let panelHeight = panelHeight {
self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: .animated(duration: 0.2, curve: .easeInOut))
let _ = self.updateFieldAndButtonsLayout(inputHasText: inputHasText, panelHeight: panelHeight, transition: .animated(duration: 0.2, curve: .easeInOut))
}
}

View file

@ -13,6 +13,8 @@ public func emojiFlagForISOCountryCode(_ countryCode: String) -> String {
return "🛰️"
} else if countryCode == "XV" {
return "🌍"
} else if countryCode == "TS" {
return "🏳️"
}
if ["YL"].contains(countryCode) {

View file

@ -2376,7 +2376,6 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
//if self. {
//TODO:localize
//self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Add", badge: "\(count)", font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: count > 0, progress: .none, isEnabled: true, hasShimmer: false)))
//}
}

View file

@ -1302,7 +1302,6 @@ private final class DemoSheetContent: CombinedComponent {
buttonAnimationName = "premium_unlock"
case .paidMessages:
buttonText = strings.Premium_PaidMessages_Proceed
buttonAnimationName = "premium_unlock"
default:
buttonText = strings.Common_OK
}

View file

@ -1470,16 +1470,15 @@ public func privacyAndSecurityController(
}
showPrivacySuggestionImpl = {
//TODO:localize
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if reviewCallPrivacySuggestion {
reviewCallPrivacySuggestion = false
let alertController = textAlertController(
context: context,
title: "Call Settings",
text: "You've restricted who can message you, but anyone can still call you. Would you like to review these settings?",
title: presentationData.strings.Privacy_Review_Calls_Title,
text: presentationData.strings.Privacy_Review_Calls_Text,
actions: [
TextAlertAction(type: .defaultAction, title: "Review", action: {
TextAlertAction(type: .defaultAction, title: presentationData.strings.Privacy_Review_Review, action: {
arguments.openVoiceCallPrivacy()
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
@ -1495,10 +1494,10 @@ public func privacyAndSecurityController(
reviewInvitePrivacySuggestion = false
let alertController = textAlertController(
context: context,
title: "Invitation Settings",
text: "You've restricted who can message you, but anyone can still invite you to groups and channels. Would you like to review these settings?",
title: presentationData.strings.Privacy_Review_Invite_Title,
text: presentationData.strings.Privacy_Review_Invite_Text,
actions: [
TextAlertAction(type: .defaultAction, title: "Review", action: {
TextAlertAction(type: .defaultAction, title: presentationData.strings.Privacy_Review_Review, action: {
arguments.openGroupsPrivacy()
}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {

View file

@ -250,7 +250,7 @@ private enum StatsEntry: ItemListNodeEntry {
case adsTonBalanceInfo(PresentationTheme, String)
case adsStarsBalanceTitle(PresentationTheme, String)
case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool, Int32?)
case adsStarsBalance(PresentationTheme, StarsRevenueStats, Bool, Bool, Bool, Int32?)
case adsStarsBalanceInfo(PresentationTheme, String)
case earnStarsInfo
@ -827,8 +827,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsIsEnabled, lhsCooldownUntilTimestamp):
if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsIsEnabled, rhsCooldownUntilTimestamp) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsIsEnabled == rhsIsEnabled, lhsCooldownUntilTimestamp == rhsCooldownUntilTimestamp {
case let .adsStarsBalance(lhsTheme, lhsStats, lhsCanWithdraw, lhsCanBuyAds, lhsIsEnabled, lhsCooldownUntilTimestamp):
if case let .adsStarsBalance(rhsTheme, rhsStats, rhsCanWithdraw, rhsCanBuyAds, rhsIsEnabled, rhsCooldownUntilTimestamp) = rhs, lhsTheme === rhsTheme, lhsStats == rhsStats, lhsCanWithdraw == rhsCanWithdraw, lhsCanBuyAds == rhsCanBuyAds, lhsIsEnabled == rhsIsEnabled, lhsCooldownUntilTimestamp == rhsCooldownUntilTimestamp {
return true
} else {
return false
@ -1108,7 +1108,7 @@ private enum StatsEntry: ItemListNodeEntry {
return ItemListTextItem(presentationData: presentationData, text: .markdown(text), sectionId: self.section, linkAction: { _ in
arguments.openMonetizationInfo()
})
case let .adsStarsBalance(_, stats, canWithdraw, isEnabled, cooldownUntilTimestamp):
case let .adsStarsBalance(_, stats, canWithdraw, canBuyAds, isEnabled, cooldownUntilTimestamp):
return MonetizationBalanceItem(
context: arguments.context,
presentationData: presentationData,
@ -1131,7 +1131,7 @@ private enum StatsEntry: ItemListNodeEntry {
arguments.requestStarsWithdraw()
}
},
buyAdsAction: canWithdraw ? {
buyAdsAction: canWithdraw && canBuyAds ? {
arguments.buyAds()
} : nil,
sectionId: self.section,
@ -1565,7 +1565,7 @@ private func monetizationEntries(
presentationData: PresentationData,
state: ChannelStatsControllerState,
peer: EnginePeer?,
data: RevenueStats,
data: RevenueStats?,
boostData: ChannelBoostStatus?,
transactionsInfo: RevenueStatsTransactionsContext.State,
starsData: StarsRevenueStats?,
@ -1584,7 +1584,7 @@ private func monetizationEntries(
isBot = true
}
if canViewRevenue {
if canViewRevenue, let data {
entries.append(.adsHeader(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_Header : presentationData.strings.Monetization_Header))
if !data.topHoursGraph.isEmpty {
@ -1608,7 +1608,7 @@ private func monetizationEntries(
entries.append(.adsProceedsTitle(presentationData.theme, presentationData.strings.Monetization_StarsProceeds_Title))
entries.append(.adsProceedsOverview(presentationData.theme, canViewRevenue ? data : nil, canViewStarsRevenue ? starsData : nil))
let hasTonBalance = data.balances.overallRevenue > 0
let hasTonBalance = (data?.balances.overallRevenue ?? 0) > 0
let hasStarsBalance = (starsData?.balances.overallRevenue ?? StarsAmount.zero) > StarsAmount.zero
let proceedsInfo: String
@ -1622,11 +1622,15 @@ private func monetizationEntries(
entries.append(.adsProceedsInfo(presentationData.theme, proceedsInfo))
var isCreator = false
var isGroup = false
if let peer, case let .channel(channel) = peer, channel.flags.contains(.isCreator) {
isCreator = true
if case .group = channel.info {
isGroup = true
}
}
if canViewRevenue {
if canViewRevenue, let data {
entries.append(.adsTonBalanceTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_Bot_BalanceTitle : presentationData.strings.Monetization_TonBalanceTitle))
entries.append(.adsTonBalance(presentationData.theme, data, (isCreator || isBot) && data.balances.availableBalance > 0, data.balances.withdrawEnabled))
@ -1643,12 +1647,14 @@ private func monetizationEntries(
}
}
if canViewStarsRevenue {
if let starsData, starsData.balances.overallRevenue > StarsAmount.zero {
entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle))
entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > StarsAmount.zero, starsData.balances.withdrawEnabled, starsData.balances.nextWithdrawalTimestamp))
entries.append(.adsStarsBalanceInfo(presentationData.theme, presentationData.strings.Monetization_Balance_StarsInfo))
}
if canViewStarsRevenue, let starsData, starsData.balances.overallRevenue > StarsAmount.zero {
entries.append(.adsStarsBalanceTitle(presentationData.theme, presentationData.strings.Monetization_StarsBalanceTitle))
entries.append(.adsStarsBalance(presentationData.theme, starsData, isCreator && starsData.balances.availableBalance > StarsAmount.zero, !isGroup, starsData.balances.withdrawEnabled, starsData.balances.nextWithdrawalTimestamp))
entries.append(.adsStarsBalanceInfo(presentationData.theme, isGroup ? presentationData.strings.Monetization_Balance_StarsInfoGroup : presentationData.strings.Monetization_Balance_StarsInfo))
}
if canJoinRefPrograms && !isGroup {
entries.append(.earnStarsInfo)
}
var addedTransactionsTabs = false
@ -1666,7 +1672,7 @@ private func monetizationEntries(
if canViewStarsRevenue && !starsTransactionsInfo.transactions.isEmpty && (transactionsInfo.transactions.isEmpty || state.starsSelected) {
displayStarsTransactions = true
}
if displayTonTransactions {
if !addedTransactionsTabs {
entries.append(.adsTransactionsTitle(presentationData.theme, isBot ? presentationData.strings.Monetization_TransactionsTitle.uppercased() : presentationData.strings.Monetization_TonTransactions.uppercased()))
@ -1700,11 +1706,7 @@ private func monetizationEntries(
if displayStarsTransactions {
if !addedTransactionsTabs {
if canJoinRefPrograms {
entries.append(.earnStarsInfo)
}
entries.append(.adsTransactionsTitle(presentationData.theme, presentationData.strings.Monetization_StarsTransactions.uppercased()))
entries.append(.adsTransactionsTitle(presentationData.theme, isGroup ? presentationData.strings.Monetization_TransactionsTitle.uppercased() : presentationData.strings.Monetization_StarsTransactions.uppercased()))
}
var transactions = starsTransactionsInfo.transactions
@ -1733,7 +1735,7 @@ private func monetizationEntries(
}
}
if isCreator && canViewRevenue {
if isCreator && canViewRevenue && !isGroup {
var switchOffAdds: Bool? = nil
if let boostData, boostData.level >= premiumConfiguration.minChannelRestrictAdsLevel {
switchOffAdds = adsRestricted
@ -1797,7 +1799,7 @@ private func channelStatsControllerEntries(
)
}
case .monetization:
if let revenueState {
if revenueState != nil || starsState != nil {
return monetizationEntries(
presentationData: presentationData,
state: state,
@ -1825,6 +1827,7 @@ public func channelStatsController(
peerId: PeerId,
section: ChannelStatsSection = .stats,
existingRevenueContext: RevenueStatsContext? = nil,
existingStarsRevenueContext: StarsRevenueStatsContext? = nil,
boostStatus: ChannelBoostStatus? = nil,
boostStatusUpdated: ((ChannelBoostStatus) -> Void)? = nil
) -> ViewController {
@ -1888,7 +1891,7 @@ public func channelStatsController(
let revenueState = Promise<RevenueStatsContextState?>()
revenueState.set(.single(nil) |> then(revenueContext.state |> map(Optional.init)))
let starsContext = context.engine.payments.peerStarsRevenueContext(peerId: peerId)
let starsContext = existingStarsRevenueContext ?? context.engine.payments.peerStarsRevenueContext(peerId: peerId)
let starsState = Promise<StarsRevenueStatsContextState?>()
starsState.set(.single(nil) |> then(starsContext.state |> map(Optional.init)))
@ -2157,8 +2160,9 @@ public func channelStatsController(
)
|> deliverOnMainQueue
|> map { presentationData, state, peer, data, messageView, stories, boostData, boostersState, giftsState, revenueState, revenueTransactions, starsState, starsTransactions, peerData, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
let (canViewStats, adsRestricted, _, canViewStarsRevenue) = peerData
let (canViewStats, adsRestricted, _, _) = peerData
var canViewRevenue = peerData.2
var canViewStarsRevenue = peerData.3
var canJoinRefPrograms = false
if let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["starref_connect_allowed"] {
@ -2192,7 +2196,7 @@ public func channelStatsController(
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
case .monetization:
if revenueState?.stats == nil {
if revenueState?.stats == nil && starsState?.stats == nil {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
}
}
@ -2222,7 +2226,10 @@ public func channelStatsController(
var headerItem: BoostHeaderItem?
var leftNavigationButton: ItemListNavigationButton?
var boostsOnly = false
if existingRevenueContext != nil {
if existingStarsRevenueContext != nil {
title = .text(presentationData.strings.Stats_Monetization)
canViewStarsRevenue = true
} else if existingRevenueContext != nil {
title = .text(presentationData.strings.Stats_TonBotRevenue_Title)
canViewRevenue = true
} else if section == .boosts {

View file

@ -231,7 +231,10 @@ final class StarsTransactionItemNode: ListViewItemNode, ItemListItemNode {
var itemDate: String
switch item.transaction.peer {
case let .peer(peer):
if !item.transaction.media.isEmpty {
if item.transaction.flags.contains(.isPaidMessage) {
itemTitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
itemSubtitle = item.presentationData.strings.Stars_Intro_Transaction_PaidMessage(item.transaction.paidMessageCount ?? 1)
} else if !item.transaction.media.isEmpty {
itemTitle = item.presentationData.strings.Stars_Intro_Transaction_MediaPurchase
itemSubtitle = peer.displayTitle(strings: item.presentationData.strings, displayOrder: .firstLast)
} else if let title = item.transaction.title {

View file

@ -734,7 +734,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-901375139] = { return Api.PeerLocated.parse_peerLocated($0) }
dict[-118740917] = { return Api.PeerLocated.parse_peerSelfLocated($0) }
dict[-1721619444] = { return Api.PeerNotifySettings.parse_peerNotifySettings($0) }
dict[227293676] = { return Api.PeerSettings.parse_peerSettings($0) }
dict[-193510921] = { return Api.PeerSettings.parse_peerSettings($0) }
dict[-1707742823] = { return Api.PeerStories.parse_peerStories($0) }
dict[1000707084] = { return Api.PhoneCall.parse_phoneCall($0) }
dict[587035009] = { return Api.PhoneCall.parse_phoneCallAccepted($0) }
@ -1390,7 +1390,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) }
dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) }
dict[-74456004] = { return Api.payments.SavedInfo.parse_savedInfo($0) }
dict[-418915641] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) }
dict[-1779201615] = { return Api.payments.SavedStarGifts.parse_savedStarGifts($0) }
dict[377215243] = { return Api.payments.StarGiftUpgradePreview.parse_starGiftUpgradePreview($0) }
dict[-2069218660] = { return Api.payments.StarGiftWithdrawalUrl.parse_starGiftWithdrawalUrl($0) }
dict[-1877571094] = { return Api.payments.StarGifts.parse_starGifts($0) }

View file

@ -960,13 +960,13 @@ public extension Api {
}
public extension Api {
enum PeerSettings: TypeConstructorDescription {
case peerSettings(flags: Int32, geoDistance: Int32?, requestChatTitle: String?, requestChatDate: Int32?, businessBotId: Int64?, businessBotManageUrl: String?, chargePaidMessageStars: Int64?, registrationMonth: String?, phoneCountry: String?, locationCountry: String?)
case peerSettings(flags: Int32, geoDistance: Int32?, requestChatTitle: String?, requestChatDate: Int32?, businessBotId: Int64?, businessBotManageUrl: String?, chargePaidMessageStars: Int64?, registrationMonth: String?, phoneCountry: String?, nameChangeDate: Int32?, photoChangeDate: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl, let chargePaidMessageStars, let registrationMonth, let phoneCountry, let locationCountry):
case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl, let chargePaidMessageStars, let registrationMonth, let phoneCountry, let nameChangeDate, let photoChangeDate):
if boxed {
buffer.appendInt32(227293676)
buffer.appendInt32(-193510921)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 6) != 0 {serializeInt32(geoDistance!, buffer: buffer, boxed: false)}
@ -977,15 +977,16 @@ public extension Api {
if Int(flags) & Int(1 << 14) != 0 {serializeInt64(chargePaidMessageStars!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 15) != 0 {serializeString(registrationMonth!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 16) != 0 {serializeString(phoneCountry!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 17) != 0 {serializeString(locationCountry!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 17) != 0 {serializeInt32(nameChangeDate!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 18) != 0 {serializeInt32(photoChangeDate!, buffer: buffer, boxed: false)}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl, let chargePaidMessageStars, let registrationMonth, let phoneCountry, let locationCountry):
return ("peerSettings", [("flags", flags as Any), ("geoDistance", geoDistance as Any), ("requestChatTitle", requestChatTitle as Any), ("requestChatDate", requestChatDate as Any), ("businessBotId", businessBotId as Any), ("businessBotManageUrl", businessBotManageUrl as Any), ("chargePaidMessageStars", chargePaidMessageStars as Any), ("registrationMonth", registrationMonth as Any), ("phoneCountry", phoneCountry as Any), ("locationCountry", locationCountry as Any)])
case .peerSettings(let flags, let geoDistance, let requestChatTitle, let requestChatDate, let businessBotId, let businessBotManageUrl, let chargePaidMessageStars, let registrationMonth, let phoneCountry, let nameChangeDate, let photoChangeDate):
return ("peerSettings", [("flags", flags as Any), ("geoDistance", geoDistance as Any), ("requestChatTitle", requestChatTitle as Any), ("requestChatDate", requestChatDate as Any), ("businessBotId", businessBotId as Any), ("businessBotManageUrl", businessBotManageUrl as Any), ("chargePaidMessageStars", chargePaidMessageStars as Any), ("registrationMonth", registrationMonth as Any), ("phoneCountry", phoneCountry as Any), ("nameChangeDate", nameChangeDate as Any), ("photoChangeDate", photoChangeDate as Any)])
}
}
@ -1008,8 +1009,10 @@ public extension Api {
if Int(_1!) & Int(1 << 15) != 0 {_8 = parseString(reader) }
var _9: String?
if Int(_1!) & Int(1 << 16) != 0 {_9 = parseString(reader) }
var _10: String?
if Int(_1!) & Int(1 << 17) != 0 {_10 = parseString(reader) }
var _10: Int32?
if Int(_1!) & Int(1 << 17) != 0 {_10 = reader.readInt32() }
var _11: Int32?
if Int(_1!) & Int(1 << 18) != 0 {_11 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 6) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 9) == 0) || _3 != nil
@ -1020,8 +1023,9 @@ public extension Api {
let _c8 = (Int(_1!) & Int(1 << 15) == 0) || _8 != nil
let _c9 = (Int(_1!) & Int(1 << 16) == 0) || _9 != nil
let _c10 = (Int(_1!) & Int(1 << 17) == 0) || _10 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 {
return Api.PeerSettings.peerSettings(flags: _1!, geoDistance: _2, requestChatTitle: _3, requestChatDate: _4, businessBotId: _5, businessBotManageUrl: _6, chargePaidMessageStars: _7, registrationMonth: _8, phoneCountry: _9, locationCountry: _10)
let _c11 = (Int(_1!) & Int(1 << 18) == 0) || _11 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 {
return Api.PeerSettings.peerSettings(flags: _1!, geoDistance: _2, requestChatTitle: _3, requestChatDate: _4, businessBotId: _5, businessBotManageUrl: _6, chargePaidMessageStars: _7, registrationMonth: _8, phoneCountry: _9, nameChangeDate: _10, photoChangeDate: _11)
}
else {
return nil

View file

@ -1,21 +1,16 @@
public extension Api.payments {
enum SavedStarGifts: TypeConstructorDescription {
case savedStarGifts(flags: Int32, count: Int32, chatNotificationsEnabled: Api.Bool?, pinnedToTop: [Int64]?, gifts: [Api.SavedStarGift], nextOffset: String?, chats: [Api.Chat], users: [Api.User])
case savedStarGifts(flags: Int32, count: Int32, chatNotificationsEnabled: Api.Bool?, gifts: [Api.SavedStarGift], nextOffset: String?, chats: [Api.Chat], users: [Api.User])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let pinnedToTop, let gifts, let nextOffset, let chats, let users):
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let gifts, let nextOffset, let chats, let users):
if boxed {
buffer.appendInt32(-418915641)
buffer.appendInt32(-1779201615)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(count, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 1) != 0 {chatNotificationsEnabled!.serialize(buffer, true)}
if Int(flags) & Int(1 << 2) != 0 {buffer.appendInt32(481674261)
buffer.appendInt32(Int32(pinnedToTop!.count))
for item in pinnedToTop! {
serializeInt64(item, buffer: buffer, boxed: false)
}}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(gifts.count))
for item in gifts {
@ -38,8 +33,8 @@ public extension Api.payments {
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let pinnedToTop, let gifts, let nextOffset, let chats, let users):
return ("savedStarGifts", [("flags", flags as Any), ("count", count as Any), ("chatNotificationsEnabled", chatNotificationsEnabled as Any), ("pinnedToTop", pinnedToTop as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)])
case .savedStarGifts(let flags, let count, let chatNotificationsEnabled, let gifts, let nextOffset, let chats, let users):
return ("savedStarGifts", [("flags", flags as Any), ("count", count as Any), ("chatNotificationsEnabled", chatNotificationsEnabled as Any), ("gifts", gifts as Any), ("nextOffset", nextOffset as Any), ("chats", chats as Any), ("users", users as Any)])
}
}
@ -52,34 +47,29 @@ public extension Api.payments {
if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() {
_3 = Api.parse(reader, signature: signature) as? Api.Bool
} }
var _4: [Int64]?
if Int(_1!) & Int(1 << 2) != 0 {if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 570911930, elementType: Int64.self)
} }
var _5: [Api.SavedStarGift]?
var _4: [Api.SavedStarGift]?
if let _ = reader.readInt32() {
_5 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedStarGift.self)
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.SavedStarGift.self)
}
var _6: String?
if Int(_1!) & Int(1 << 0) != 0 {_6 = parseString(reader) }
var _7: [Api.Chat]?
var _5: String?
if Int(_1!) & Int(1 << 0) != 0 {_5 = parseString(reader) }
var _6: [Api.Chat]?
if let _ = reader.readInt32() {
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
_6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Chat.self)
}
var _8: [Api.User]?
var _7: [Api.User]?
if let _ = reader.readInt32() {
_8 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
_7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
let _c4 = (Int(_1!) & Int(1 << 2) == 0) || _4 != nil
let _c5 = _5 != nil
let _c6 = (Int(_1!) & Int(1 << 0) == 0) || _6 != nil
let _c4 = _4 != nil
let _c5 = (Int(_1!) & Int(1 << 0) == 0) || _5 != nil
let _c6 = _6 != nil
let _c7 = _7 != nil
let _c8 = _8 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 {
return Api.payments.SavedStarGifts.savedStarGifts(flags: _1!, count: _2!, chatNotificationsEnabled: _3, pinnedToTop: _4, gifts: _5!, nextOffset: _6, chats: _7!, users: _8!)
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 {
return Api.payments.SavedStarGifts.savedStarGifts(flags: _1!, count: _2!, chatNotificationsEnabled: _3, gifts: _4!, nextOffset: _5, chats: _6!, users: _7!)
}
else {
return nil

View file

@ -155,7 +155,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
text = nil
entities = nil
}
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 5)) != 0, boostPeerId: boostPeer?.peerId, months: months, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
case let .messageActionGiveawayLaunch(_, stars):
return TelegramMediaAction(action: .giveawayLaunched(stars: stars))
case let .messageActionGiveawayResults(flags, winners, unclaimed):

View file

@ -6,7 +6,7 @@ import SwiftSignalKit
extension PeerStatusSettings {
init(apiSettings: Api.PeerSettings) {
switch apiSettings {
case let .peerSettings(flags, geoDistance, requestChatTitle, requestChatDate, businessBotId, businessBotManageUrl, chargePaidMessageStars, registrationMonth, phoneCountry, locationCountry):
case let .peerSettings(flags, geoDistance, requestChatTitle, requestChatDate, businessBotId, businessBotManageUrl, chargePaidMessageStars, registrationMonth, phoneCountry, nameChangeDate, photoChangeDate):
var result = PeerStatusSettings.Flags()
if (flags & (1 << 1)) != 0 {
result.insert(.canAddContact)
@ -54,7 +54,8 @@ extension PeerStatusSettings {
paidMessageStars: chargePaidMessageStars.flatMap { StarsAmount(value: $0, nanos: 0) },
registrationDate: registrationMonth,
phoneCountry: phoneCountry,
locationCountry: locationCountry
nameChangeDate: nameChangeDate,
photoChangeDate: photoChangeDate
)
}
}

View file

@ -304,7 +304,7 @@ private func requestChannelStats(postbox: Postbox, network: Network, peerId: Pee
}
return nil
} |> mapToSignal { data -> Signal<ChannelStats?, NoError> in
guard let (statsDatacenterId, peer) = data, let inputChannel = apiInputChannel(peer) else {
guard let (statsDatacenterId, peer) = data, let channel = peer as? TelegramChannel, case .broadcast = channel.info, let inputChannel = apiInputChannel(peer) else {
return .never()
}
@ -813,7 +813,7 @@ private func requestGroupStats(accountPeerId: PeerId, postbox: Postbox, network:
}
return nil
} |> mapToSignal { data -> Signal<GroupStats?, NoError> in
guard let (statsDatacenterId, peer) = data, let inputChannel = apiInputChannel(peer) else {
guard let (statsDatacenterId, peer) = data, let channel = peer as? TelegramChannel, case .group = channel.info, let inputChannel = apiInputChannel(peer) else {
return .never()
}

View file

@ -145,7 +145,7 @@ private func requestRevenueStats(postbox: Postbox, network: Network, peerId: Pee
}
return nil
} |> mapToSignal { peer -> Signal<RevenueStats?, NoError> in
guard let peer, let inputPeer = apiInputPeer(peer) else {
guard let peer, let channel = peer as? TelegramChannel, case .broadcast = channel.info, let inputPeer = apiInputPeer(peer) else {
return .never()
}
@ -354,7 +354,7 @@ private final class RevenueStatsTransactionsContextImpl {
}
|> mapToSignal { peer -> Signal<([RevenueStatsTransactionsContext.State.Transaction], Int32, Int32?), NoError> in
if let peer {
guard let inputPeer = apiInputPeer(peer) else {
guard let channel = peer as? TelegramChannel, case .broadcast = channel.info, let inputPeer = apiInputPeer(peer) else {
return .complete()
}
let offset = lastOffset ?? 0

View file

@ -141,6 +141,7 @@ public struct Namespaces {
public static let cachedProfileGifts: Int8 = 43
public static let recommendedBots: Int8 = 44
public static let channelsForPublicReaction: Int8 = 45
public static let cachedGroupsInCommon: Int8 = 46
}
public struct UnorderedItemList {

View file

@ -42,7 +42,8 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
public var paidMessageStars: StarsAmount?
public var registrationDate: String?
public var phoneCountry: String?
public var locationCountry: String?
public var nameChangeDate: Int32?
public var photoChangeDate: Int32?
public init() {
self.flags = PeerStatusSettings.Flags()
@ -53,7 +54,8 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
self.paidMessageStars = nil
self.registrationDate = nil
self.phoneCountry = nil
self.locationCountry = nil
self.nameChangeDate = nil
self.photoChangeDate = nil
}
public init(
@ -66,7 +68,8 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
paidMessageStars: StarsAmount? = nil,
registrationDate: String? = nil,
phoneCountry: String? = nil,
locationCountry: String? = nil
nameChangeDate: Int32? = nil,
photoChangeDate: Int32? = nil
) {
self.flags = flags
self.geoDistance = geoDistance
@ -77,7 +80,8 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
self.paidMessageStars = paidMessageStars
self.registrationDate = registrationDate
self.phoneCountry = phoneCountry
self.locationCountry = locationCountry
self.nameChangeDate = nameChangeDate
self.photoChangeDate = photoChangeDate
}
public init(decoder: PostboxDecoder) {
@ -90,7 +94,8 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
self.paidMessageStars = decoder.decodeCodable(StarsAmount.self, forKey: "paidMessageStars")
self.registrationDate = decoder.decodeOptionalStringForKey("registrationDate")
self.phoneCountry = decoder.decodeOptionalStringForKey("phoneCountry")
self.locationCountry = decoder.decodeOptionalStringForKey("locationCountry")
self.nameChangeDate = decoder.decodeOptionalInt32ForKey("nameChangeDate")
self.photoChangeDate = decoder.decodeOptionalInt32ForKey("photoChangeDate")
}
public func encode(_ encoder: PostboxEncoder) {
@ -135,10 +140,15 @@ public struct PeerStatusSettings: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "phoneCountry")
}
if let locationCountry = self.locationCountry {
encoder.encodeString(locationCountry, forKey: "locationCountry")
if let nameChangeDate = self.nameChangeDate {
encoder.encodeInt32(nameChangeDate, forKey: "nameChangeDate")
} else {
encoder.encodeNil(forKey: "locationCountry")
encoder.encodeNil(forKey: "nameChangeDate")
}
if let photoChangeDate = self.photoChangeDate {
encoder.encodeInt32(photoChangeDate, forKey: "photoChangeDate")
} else {
encoder.encodeNil(forKey: "photoChangeDate")
}
}

View file

@ -109,7 +109,7 @@ func _internal_peerSendAsAvailablePeers(accountPeerId: PeerId, network: Network,
return .single([])
}
if let _ = peer as? TelegramChannel {
if let channel = peer as? TelegramChannel, case .group = channel.info {
} else {
return .single([])
}

View file

@ -745,6 +745,25 @@ func _internal_updateStarGiftAddedToProfile(account: Account, reference: StarGif
}
}
func _internal_updateStarGiftsPinnedToTop(account: Account, peerId: EnginePeer.Id, references: [StarGiftReference]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction in
let peer = transaction.getPeer(peerId)
let starGifts = references.compactMap { $0.apiStarGiftReference(transaction: transaction) }
return (peer, starGifts)
}
|> mapToSignal { peer, starGifts in
guard let inputPeer = peer.flatMap(apiInputPeer) else {
return .complete()
}
return account.network.request(Api.functions.payments.toggleStarGiftsPinnedToTop(peer: inputPeer, stargift: starGifts))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Bool?, NoError> in
return .single(nil)
}
|> ignoreValues
}
}
public enum TransferStarGiftError {
case generic
}
@ -854,6 +873,7 @@ func _internal_upgradeStarGift(account: Account, formId: Int64?, reference: Star
entities: nil,
nameHidden: false,
savedToProfile: savedToProfile,
pinnedToTop: false,
convertStars: nil,
canUpgrade: false,
canExportDate: canExportDate,
@ -1000,7 +1020,7 @@ private final class ProfileGiftsContextImpl {
let dataState = isFiltered ? self.filteredDataState : self.dataState
if case let .ready(true, initialNextOffset) = dataState {
if !isFiltered, self.gifts.isEmpty, initialNextOffset == nil {
if !isFiltered, self.gifts.isEmpty, initialNextOffset == nil, !reload {
self.cacheDisposable.set((self.account.postbox.transaction { transaction -> CachedProfileGifts? in
let cachedGifts = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedProfileGifts.self)
cachedGifts?.render(transaction: transaction)
@ -1064,8 +1084,7 @@ private final class ProfileGiftsContextImpl {
}
return postbox.transaction { transaction -> ([ProfileGiftsContext.State.StarGift], Int32, String?, Bool?) in
switch result {
case let .savedStarGifts(_, count, apiNotificationsEnabled, pinnedToTop, apiGifts, nextOffset, chats, users):
let _ = pinnedToTop
case let .savedStarGifts(_, count, apiNotificationsEnabled, apiGifts, nextOffset, chats, users):
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
@ -1131,9 +1150,33 @@ private final class ProfileGiftsContextImpl {
self.actionDisposable.set(
_internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: added).startStrict()
)
if let index = self.gifts.firstIndex(where: { $0.reference == reference }) {
self.gifts[index] = self.gifts[index].withSavedToProfile(added)
if !added && self.gifts[index].pinnedToTop {
let pinnedGifts = self.gifts.filter { $0.pinnedToTop && $0.reference != reference }
let existingGifts = Set(pinnedGifts.compactMap { $0.reference })
var updatedGifts: [ProfileGiftsContext.State.StarGift] = []
for gift in self.gifts {
if let reference = gift.reference, existingGifts.contains(reference) {
continue
}
var gift = gift
if gift.reference == reference {
gift = gift.withPinnedToTop(false).withSavedToProfile(false)
}
updatedGifts.append(gift)
}
updatedGifts.sort { lhs, rhs in
lhs.date > rhs.date
}
updatedGifts.insert(contentsOf: pinnedGifts, at: 0)
self.gifts = updatedGifts
} else {
self.gifts[index] = self.gifts[index].withSavedToProfile(added)
}
}
if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) {
self.filteredGifts[index] = self.filteredGifts[index].withSavedToProfile(added)
if !self.filter.contains(.hidden) && !added {
@ -1143,6 +1186,107 @@ private final class ProfileGiftsContextImpl {
self.pushState()
}
func updateStarGiftPinnedToTop(reference: StarGiftReference, pinnedToTop: Bool) {
var pinnedGifts = self.gifts.filter { $0.pinnedToTop }
var saveToProfile = false
if var gift = self.gifts.first(where: { $0.reference == reference }) {
gift = gift.withPinnedToTop(pinnedToTop)
if pinnedToTop {
if !gift.savedToProfile {
gift = gift.withSavedToProfile(true)
saveToProfile = true
}
pinnedGifts.append(gift)
} else {
pinnedGifts.removeAll(where: { $0.reference == reference })
}
}
let existingGifts = Set(pinnedGifts.compactMap { $0.reference })
var updatedGifts: [ProfileGiftsContext.State.StarGift] = []
for gift in self.gifts {
if let reference = gift.reference, existingGifts.contains(reference) {
continue
}
updatedGifts.append(gift)
}
updatedGifts.sort { lhs, rhs in
lhs.date > rhs.date
}
updatedGifts.insert(contentsOf: pinnedGifts, at: 0)
self.gifts = updatedGifts
if let index = self.filteredGifts.firstIndex(where: { $0.reference == reference }) {
self.filteredGifts[index] = self.filteredGifts[index].withPinnedToTop(pinnedToTop)
}
self.pushState()
var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference })
if saveToProfile {
signal = _internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true)
|> then(signal)
}
self.actionDisposable.set(
(signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in
self?.reload()
})
)
}
public func updatePinnedToTopStarGifts(references: [StarGiftReference]) {
let existingGifts = Set(references)
var saveSignals: [Signal<Never, NoError>] = []
let currentPinnedGifts = self.gifts.filter { gift in
if let reference = gift.reference {
return existingGifts.contains(reference)
} else {
return false
}
}.map { gift in
if !gift.savedToProfile, let reference = gift.reference {
saveSignals.append(_internal_updateStarGiftAddedToProfile(account: self.account, reference: reference, added: true))
}
return gift.withPinnedToTop(true).withSavedToProfile(true)
}
var updatedGifts: [ProfileGiftsContext.State.StarGift] = []
for gift in self.gifts {
if let reference = gift.reference, existingGifts.contains(reference) {
continue
}
updatedGifts.append(gift.withPinnedToTop(false))
}
updatedGifts.sort { lhs, rhs in
lhs.date > rhs.date
}
var pinnedGifts: [ProfileGiftsContext.State.StarGift] = []
for reference in references {
if let gift = currentPinnedGifts.first(where: { $0.reference == reference }) {
pinnedGifts.append(gift)
}
}
updatedGifts.insert(contentsOf: pinnedGifts, at: 0)
self.gifts = updatedGifts
self.pushState()
var signal = _internal_updateStarGiftsPinnedToTop(account: self.account, peerId: self.peerId, references: pinnedGifts.compactMap { $0.reference })
if !saveSignals.isEmpty {
signal = combineLatest(saveSignals)
|> ignoreValues
|> then(signal)
}
self.actionDisposable.set(
(signal |> deliverOn(self.queue)).startStrict(completed: { [weak self] in
self?.reload()
})
)
}
func convertStarGift(reference: StarGiftReference) {
self.actionDisposable.set(
_internal_convertStarGift(account: self.account, reference: reference).startStrict()
@ -1277,6 +1421,7 @@ public final class ProfileGiftsContext {
case messageId
case nameHidden
case savedToProfile
case pinnedToTop
case convertStars
case canUpgrade
case canExportDate
@ -1293,6 +1438,7 @@ public final class ProfileGiftsContext {
public let entities: [MessageTextEntity]?
public let nameHidden: Bool
public let savedToProfile: Bool
public let pinnedToTop: Bool
public let convertStars: Int64?
public let canUpgrade: Bool
public let canExportDate: Int32?
@ -1314,6 +1460,7 @@ public final class ProfileGiftsContext {
entities: [MessageTextEntity]?,
nameHidden: Bool,
savedToProfile: Bool,
pinnedToTop: Bool,
convertStars: Int64?,
canUpgrade: Bool,
canExportDate: Int32?,
@ -1329,6 +1476,7 @@ public final class ProfileGiftsContext {
self.entities = entities
self.nameHidden = nameHidden
self.savedToProfile = savedToProfile
self.pinnedToTop = pinnedToTop
self.convertStars = convertStars
self.canUpgrade = canUpgrade
self.canExportDate = canExportDate
@ -1354,6 +1502,7 @@ public final class ProfileGiftsContext {
self.entities = try container.decodeIfPresent([MessageTextEntity].self, forKey: .entities)
self.nameHidden = try container.decode(Bool.self, forKey: .nameHidden)
self.savedToProfile = try container.decode(Bool.self, forKey: .savedToProfile)
self.pinnedToTop = try container.decodeIfPresent(Bool.self, forKey: .pinnedToTop) ?? false
self.convertStars = try container.decodeIfPresent(Int64.self, forKey: .convertStars)
self.canUpgrade = try container.decode(Bool.self, forKey: .canUpgrade)
self.canExportDate = try container.decodeIfPresent(Int32.self, forKey: .canExportDate)
@ -1372,6 +1521,7 @@ public final class ProfileGiftsContext {
try container.encodeIfPresent(self.entities, forKey: .entities)
try container.encode(self.nameHidden, forKey: .nameHidden)
try container.encode(self.savedToProfile, forKey: .savedToProfile)
try container.encode(self.pinnedToTop, forKey: .pinnedToTop)
try container.encodeIfPresent(self.convertStars, forKey: .convertStars)
try container.encode(self.canUpgrade, forKey: .canUpgrade)
try container.encodeIfPresent(self.canExportDate, forKey: .canExportDate)
@ -1389,6 +1539,7 @@ public final class ProfileGiftsContext {
entities: self.entities,
nameHidden: self.nameHidden,
savedToProfile: savedToProfile,
pinnedToTop: self.pinnedToTop,
convertStars: self.convertStars,
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
@ -1397,6 +1548,24 @@ public final class ProfileGiftsContext {
)
}
public func withPinnedToTop(_ pinnedToTop: Bool) -> StarGift {
return StarGift(
gift: self.gift,
reference: self.reference,
fromPeer: self.fromPeer,
date: self.date,
text: self.text,
entities: self.entities,
nameHidden: self.nameHidden,
savedToProfile: self.savedToProfile,
pinnedToTop: pinnedToTop,
convertStars: self.convertStars,
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
upgradeStars: self.upgradeStars,
transferStars: self.transferStars
)
}
fileprivate func withFromPeer(_ fromPeer: EnginePeer?) -> StarGift {
return StarGift(
gift: self.gift,
@ -1406,7 +1575,8 @@ public final class ProfileGiftsContext {
text: self.text,
entities: self.entities,
nameHidden: self.nameHidden,
savedToProfile: savedToProfile,
savedToProfile: self.savedToProfile,
pinnedToTop: self.pinnedToTop,
convertStars: self.convertStars,
canUpgrade: self.canUpgrade,
canExportDate: self.canExportDate,
@ -1473,6 +1643,18 @@ public final class ProfileGiftsContext {
}
}
public func updateStarGiftPinnedToTop(reference: StarGiftReference, pinnedToTop: Bool) {
self.impl.with { impl in
impl.updateStarGiftPinnedToTop(reference: reference, pinnedToTop: pinnedToTop)
}
}
public func updatePinnedToTopStarGifts(references: [StarGiftReference]) {
self.impl.with { impl in
impl.updatePinnedToTopStarGifts(references: references)
}
}
public func convertStarGift(reference: StarGiftReference) {
self.impl.with { impl in
impl.convertStarGift(reference: reference)
@ -1569,6 +1751,7 @@ extension ProfileGiftsContext.State.StarGift {
}
self.nameHidden = (flags & (1 << 0)) != 0
self.savedToProfile = (flags & (1 << 5)) == 0
self.pinnedToTop = (flags & (1 << 12)) != 0
self.convertStars = convertStars
self.canUpgrade = (flags & (1 << 10)) != 0
self.canExportDate = canExportDate

View file

@ -1504,6 +1504,7 @@ func _internal_sendStarsPaymentForm(account: Account, formId: Int64, source: Bot
entities: nil,
nameHidden: false,
savedToProfile: savedToProfile,
pinnedToTop: false,
convertStars: nil,
canUpgrade: false,
canExportDate: canExportDate,

View file

@ -21,6 +21,7 @@ private final class GroupsInCommonContextImpl {
private let hintGroupInCommon: PeerId?
private let disposable = MetaDisposable()
private let cacheDisposable = MetaDisposable()
private var peers: [RenderedPeer] = []
private var count: Int?
@ -52,15 +53,41 @@ private final class GroupsInCommonContextImpl {
deinit {
self.disposable.dispose()
self.cacheDisposable.dispose()
}
func loadMore(limit: Int32) {
let peerId = self.peerId
if case .ready(true) = self.dataState {
if self.peers.isEmpty {
self.cacheDisposable.set((self.account.postbox.transaction { transaction -> ([RenderedPeer], Int32)? in
if let cached = transaction.retrieveItemCacheEntry(id: entryId(peerId: peerId))?.get(CachedGroupsInCommon.self) {
var peers: [RenderedPeer] = []
for peerId in cached.peerIds {
if let peer = transaction.getPeer(peerId) {
peers.append(RenderedPeer(peer: peer))
}
}
return (peers, cached.count)
}
return nil
} |> deliverOn(self.queue)).start(next: { [weak self] peersAndCount in
guard let self else {
return
}
if case .loading = self.dataState, let (peers, count) = peersAndCount {
self.peers = peers
self.count = Int(count)
self.pushState()
}
}))
}
self.dataState = .loading
self.pushState()
let maxId = self.peers.last?.peerId.id
let peerId = self.peerId
let accountPeerId = self.account.peerId
let network = self.account.network
let postbox = self.account.postbox
@ -93,7 +120,6 @@ private final class GroupsInCommonContextImpl {
count = nil
}
return postbox.transaction { transaction -> ([Peer], Int) in
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
@ -122,6 +148,14 @@ private final class GroupsInCommonContextImpl {
}
}
if maxId == nil {
strongSelf.cacheDisposable.set(postbox.transaction { transaction in
if let entry = CodableEntry(CachedGroupsInCommon(peerIds: peers.map { $0.id }, count: Int32(count))) {
transaction.putItemCacheEntry(id: entryId(peerId: peerId), entry: entry)
}
}.start())
}
let updatedCount = max(strongSelf.peers.count, count)
strongSelf.count = updatedCount
strongSelf.dataState = .ready(canLoadMore: count != 0 && updatedCount > strongSelf.peers.count)
@ -166,3 +200,38 @@ public final class GroupsInCommonContext {
}
}
}
private final class CachedGroupsInCommon: Codable {
enum CodingKeys: String, CodingKey {
case peerIds
case count
}
var peerIds: [PeerId]
let count: Int32
init(peerIds: [PeerId], count: Int32) {
self.peerIds = peerIds
self.count = count
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.peerIds = try container.decode([PeerId].self, forKey: .peerIds)
self.count = try container.decode(Int32.self, forKey: .count)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.peerIds, forKey: .peerIds)
try container.encode(self.count, forKey: .count)
}
}
private func entryId(peerId: EnginePeer.Id) -> ItemCacheEntryId {
let cacheKey = ValueBoxKey(length: 8)
cacheKey.setInt64(0, value: peerId.toInt64())
return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedGroupsInCommon, key: cacheKey)
}

View file

@ -77,6 +77,8 @@ public enum PresentationResourceKey: Int32 {
case itemListTopicArrowIcon
case itemListAddBoostsIcon
case itemListPremiumIcon
case itemListRoundTopupIcon
case itemListRoundWithdrawIcon
case statsReactionsIcon
case statsForwardsIcon
@ -314,6 +316,7 @@ public enum PresentationResourceKey: Int32 {
case chatEmptyStateStarIcon
case chatPlaceholderStarIcon
case chatUserInfoWarningIcon
case avatarPremiumLockBadgeBackground
case avatarPremiumLockBadge

View file

@ -1352,6 +1352,25 @@ public struct PresentationResourcesChat {
})
}
public static func chatUserInfoWarningIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatUserInfoWarningIcon.rawValue, { theme in
return generateImage(CGSize(width: 14.0, height: 14.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: .zero, size: size))
context.setBlendMode(.clear)
let width: CGFloat = 2.0 - UIScreenPixel
let height: CGFloat = 7.0 - UIScreenPixel
context.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - width) / 2.0), y: 2.0 + UIScreenPixel), size: CGSize(width: width, height: height)), cornerWidth: 1.0, cornerHeight: 1.0, transform: nil))
context.addPath(CGPath(ellipseIn: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - width) / 2.0), y: 2.0 + UIScreenPixel + height + 1.0 - UIScreenPixel), size: CGSize(width: width, height: width)), transform: nil))
context.fillPath()
})
})
}
public static func chatPlaceholderStarIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatPlaceholderStarIcon.rawValue, { theme in
if let image = UIImage(bundleImageName: "Premium/Stars/ButtonStar") {

View file

@ -432,4 +432,37 @@ public struct PresentationResourcesItemList {
return generateTintedImage(image: UIImage(bundleImageName: "Item List/ExpandingItemVerticalSmallArrow"), color: .white)?.withRenderingMode(.alwaysTemplate)
})
}
public static func itemListRoundTopupIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListRoundTopupIcon.rawValue, { theme in
return generateImage(CGSize(width: 16.0, height: 18.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: 0.0, y: 2.0 - UIScreenPixel)
context.setFillColor(theme.list.itemCheckColors.foregroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)))
context.setBlendMode(.clear)
context.addPath(CGPath(roundedRect: CGRect(x: 7.0, y: 3.0, width: 2.0, height: 10.0), cornerWidth: 1.0, cornerHeight: 1.0, transform: nil))
context.addPath(CGPath(roundedRect: CGRect(x: 3.0, y: 7.0, width: 10.0, height: 2.0), cornerWidth: 1.0, cornerHeight: 1.0, transform: nil))
context.fillPath()
})
})
}
public static func itemListRoundWithdrawIcon(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.itemListRoundWithdrawIcon.rawValue, { theme in
return generateImage(CGSize(width: 16.0, height: 18.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.translateBy(x: 0.0, y: 2.0 - UIScreenPixel)
context.setFillColor(theme.list.itemCheckColors.foregroundColor.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width)))
context.setBlendMode(.clear)
context.addPath(CGPath(roundedRect: CGRect(x: 3.0, y: 7.0, width: 10.0, height: 2.0), cornerWidth: 1.0, cornerHeight: 1.0, transform: nil))
context.fillPath()
})
})
}
}

View file

@ -659,3 +659,15 @@ public func stringForRemainingMuteInterval(strings: PresentationStrings, muteInt
return strings.MuteExpires_Days(Int32(round(Float(value) / (24 * 60 * 60))))
}
}
public func stringForIntervalSinceUpdateAction(strings: PresentationStrings, value: Int32) -> String {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let value = max(1 * 60, timestamp - value)
if value <= 1 * 60 * 60 {
return strings.Chat_NonContactUser_UpdatedMinutes(Int32(round(Float(value) / 60)))
} else if value <= 24 * 60 * 60 {
return strings.Chat_NonContactUser_UpdatedHours(Int32(round(Float(value) / (60 * 60))))
} else {
return strings.Chat_NonContactUser_UpdatedDays(Int32(round(Float(value) / (24 * 60 * 60))))
}
}

View file

@ -1283,8 +1283,7 @@ final class AvatarEditorScreenComponent: Component {
case .suggest:
buttonText = strings.AvatarEditor_SuggestProfilePhoto
case .user:
//TODO:localize
buttonText = "Set My Photo" //strings.AvatarEditor_SetProfilePhoto
buttonText = strings.AvatarEditor_SetProfilePhoto
case .group, .forum:
buttonText = strings.AvatarEditor_SetGroupPhoto
case .channel:

View file

@ -43,7 +43,7 @@ public struct ChatMessageEntryAttributes: Equatable {
public enum ChatInfoData: Equatable {
case botInfo(title: String, text: String, photo: TelegramMediaImage?, video: TelegramMediaFile?)
case userInfo(title: String, registrationDate: String?, phoneCountry: String?, locationCountry: String?, groupsInCommon: [EnginePeer])
case userInfo(peer: EnginePeer, verification: PeerVerification?, registrationDate: String?, phoneCountry: String?, groupsInCommonCount: Int32)
}
public enum ChatHistoryEntry: Identifiable, Comparable {

View file

@ -4597,6 +4597,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
}
}
if item.message.timestamp < 10 {
hasMenuGesture = false
}
strongSelf.mainContainerNode.isGestureEnabled = hasMenuGesture
for contentContainer in strongSelf.contentContainers {
contentContainer.containerNode.isGestureEnabled = hasMenuGesture

View file

@ -424,17 +424,9 @@ public func chatMessagePaymentAlertController(
var completionImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
//TODO:localize
let title = "Confirm Payment"
let actionTitle: String
let messagesString: String
if count > 1 {
messagesString = "**\(count)** messages"
actionTitle = "Pay for \(count) Messages"
} else {
messagesString = "**\(count)** message"
actionTitle = "Pay for 1 Message"
}
let title = presentationData.strings.Chat_PaidMessage_Confirm_Title
let actionTitle = presentationData.strings.Chat_PaidMessage_Confirm_PayForMessage(count)
let messagesString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Messages(count)
let actions: [TextAlertAction] = [TextAlertAction(type: .defaultAction, title: actionTitle, action: {
completionImpl?()
@ -442,17 +434,20 @@ public func chatMessagePaymentAlertController(
}), TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
dismissImpl?()
})]
let text: String
if peers.count == 1, let peer = peers.first {
text = "**\(peer.compactDisplayTitle)** charges **\(amount.value) Stars** per incoming message. Would you like to pay **\(amount.value * Int64(count)) Stars** to send \(messagesString)?"
let amountString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(amount.value))
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(amount.value * Int64(count)))
text = presentationData.strings.Chat_PaidMessage_Confirm_Single_Text(peer.compactDisplayTitle, amountString, totalString, messagesString).string
} else {
let amount = totalAmount ?? amount
text = "You selected **\(peers.count)** users who charge Stars for messages. Would you like to pay **\(amount.value)** Stars to send \(messagesString)?"
let usersString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Users(Int32(peers.count))
let totalString = presentationData.strings.Chat_PaidMessage_Confirm_Text_Stars(Int32(amount.value * Int64(count)))
text = presentationData.strings.Chat_PaidMessage_Confirm_Multiple_Text(usersString, totalString, messagesString).string
}
let optionText = hasCheck ? "Don't ask again" : nil
let optionText = hasCheck ? presentationData.strings.Chat_PaidMessage_Confirm_DontAskAgain : nil
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .vertical)
@ -470,8 +465,6 @@ public func chatMessagePaymentAlertController(
return controller
}
public func chatMessageRemovePaymentAlertController(
context: AccountContext? = nil,
presentationData: PresentationData,
@ -488,20 +481,19 @@ public func chatMessageRemovePaymentAlertController(
var completionImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
//TODO:localize
let actions: [TextAlertAction] = [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {
dismissImpl?()
}),
TextAlertAction(type: .defaultAction, title: "Yes", action: {
TextAlertAction(type: .defaultAction, title: strings.Chat_PaidMessage_RemoveFee_Yes, action: {
completionImpl?()
dismissImpl?()
})
]
let title = "Remove Fee"
let text = "Are you sure you want to allow **\(peer.compactDisplayTitle)** to message you for free?"
let optionText = amount.flatMap { "Refund already paid **\($0.value) Stars**" }
let title = strings.Chat_PaidMessage_RemoveFee_Title
let text = strings.Chat_PaidMessage_RemoveFee_Text(peer.compactDisplayTitle).string
let optionText = amount.flatMap { strings.Chat_PaidMessage_RemoveFee_Refund(strings.Chat_PaidMessage_RemoveFee_Refund_Stars(Int32($0.value))).string }
let contentNode = ChatMessagePaymentAlertContentNode(theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, title: title, text: text, optionText: optionText, actions: actions, alignment: .horizontal)

View file

@ -2,7 +2,11 @@ import Foundation
import UIKit
public enum ChatNavigationButtonAction: Equatable {
case openChatInfo(expandAvatar: Bool, recommendedChannels: Bool)
public enum ChatInfoSection {
case groupsInCommon
case recommendedChannels
}
case openChatInfo(expandAvatar: Bool, section: ChatInfoSection?)
case clearHistory
case clearCache
case cancelMessageSelection

View file

@ -27,6 +27,8 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageBubbleContentNode",
"//submodules/CountrySelectionUI",
"//submodules/TelegramStringFormatting",
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
"//submodules/TelegramUI/Components/TextNodeWithEntities",
],
visibility = [
"//visibility:public",

View file

@ -10,39 +10,40 @@ import TextFormat
import UrlEscaping
import PhotoResources
import AccountContext
import UniversalMediaPlayer
import TelegramUniversalVideoContent
import WallpaperBackgroundNode
import ChatControllerInteraction
import ChatMessageBubbleContentNode
import CountrySelectionUI
import TelegramStringFormatting
import MergedAvatarsNode
import ChatControllerInteraction
import TextNodeWithEntities
public final class ChatUserInfoItem: ListViewItem {
fileprivate let title: String
fileprivate let peer: EnginePeer
fileprivate let verification: PeerVerification?
fileprivate let registrationDate: String?
fileprivate let phoneCountry: String?
fileprivate let locationCountry: String?
fileprivate let groupsInCommon: [EnginePeer]
fileprivate let groupsInCommonCount: Int32
fileprivate let controllerInteraction: ChatControllerInteraction
fileprivate let presentationData: ChatPresentationData
fileprivate let context: AccountContext
public init(
title: String,
peer: EnginePeer,
verification: PeerVerification?,
registrationDate: String?,
phoneCountry: String?,
locationCountry: String?,
groupsInCommon: [EnginePeer],
groupsInCommonCount: Int32,
controllerInteraction: ChatControllerInteraction,
presentationData: ChatPresentationData,
context: AccountContext
) {
self.title = title
self.peer = peer
self.verification = verification
self.registrationDate = registrationDate
self.phoneCountry = phoneCountry
self.locationCountry = locationCountry
self.groupsInCommon = groupsInCommon
self.groupsInCommonCount = groupsInCommonCount
self.controllerInteraction = controllerInteraction
self.presentationData = presentationData
self.context = context
@ -106,11 +107,17 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
private let phoneCountryValueTextNode: TextNode
private var phoneCountryText: String?
private let locationCountryTitleTextNode: TextNode
private let locationCountryValueTextNode: TextNode
private var locationCountryText: String?
private let groupsTitleTextNode: TextNode
private let groupsValueTextNode: TextNode
private let groupsButtonNode: HighlightTrackingButtonNode
private let groupsAvatarsNode: MergedAvatarsNode
private let groupsArrowNode: ASImageNode
private let groupsTextNode: TextNode
private var groupsInCommonContext: GroupsInCommonContext?
private var groupsInCommonDisposable: Disposable?
private var groupsInCommon: [Peer] = []
private let disclaimerTextNode: TextNodeWithEntities
private var theme: ChatPresentationThemeData?
@ -146,16 +153,23 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
self.phoneCountryValueTextNode.isUserInteractionEnabled = false
self.phoneCountryValueTextNode.displaysAsynchronously = false
self.locationCountryTitleTextNode = TextNode()
self.locationCountryTitleTextNode.isUserInteractionEnabled = false
self.locationCountryTitleTextNode.displaysAsynchronously = false
self.locationCountryValueTextNode = TextNode()
self.locationCountryValueTextNode.isUserInteractionEnabled = false
self.locationCountryValueTextNode.displaysAsynchronously = false
self.groupsTitleTextNode = TextNode()
self.groupsTitleTextNode.isUserInteractionEnabled = false
self.groupsTitleTextNode.displaysAsynchronously = false
self.groupsValueTextNode = TextNode()
self.groupsValueTextNode.isUserInteractionEnabled = false
self.groupsValueTextNode.displaysAsynchronously = false
self.groupsTextNode = TextNode()
self.groupsTextNode.isUserInteractionEnabled = false
self.groupsTextNode.displaysAsynchronously = false
self.groupsAvatarsNode = MergedAvatarsNode()
self.groupsArrowNode = ASImageNode()
self.groupsArrowNode.displaysAsynchronously = false
self.groupsButtonNode = HighlightTrackingButtonNode()
self.disclaimerTextNode = TextNodeWithEntities()
self.disclaimerTextNode.textNode.isUserInteractionEnabled = false
self.disclaimerTextNode.textNode.displaysAsynchronously = false
super.init(layerBacked: false, dynamicBounce: true, rotated: true)
@ -164,37 +178,38 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
self.addSubnode(self.offsetContainer)
self.offsetContainer.addSubnode(self.titleNode)
self.offsetContainer.addSubnode(self.subtitleNode)
self.offsetContainer.addSubnode(self.groupsTextNode)
self.offsetContainer.addSubnode(self.disclaimerTextNode.textNode)
self.offsetContainer.addSubnode(self.groupsAvatarsNode)
self.offsetContainer.addSubnode(self.groupsArrowNode)
self.offsetContainer.addSubnode(self.groupsButtonNode)
self.wantsTrailingItemSpaceUpdates = true
}
override public func didLoad() {
super.didLoad()
// let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
// recognizer.tapActionAtPoint = { [weak self] point in
// if let strongSelf = self {
// let tapAction = strongSelf.tapActionAtPoint(point, gesture: .tap, isEstimating: true)
// switch tapAction.content {
// case .none:
// break
// case .ignore:
// return .fail
// case .url, .phone, .peerMention, .textMention, .botCommand, .hashtag, .instantPage, .wallpaper, .theme, .call, .openMessage, .timecode, .bankCard, .tooltip, .openPollResults, .copy, .largeEmoji, .customEmoji, .custom:
// return .waitForSingleTap
// }
// }
//
// return .waitForDoubleTap
// }
// recognizer.highlight = { [weak self] point in
// if let strongSelf = self {
// strongSelf.updateTouchesAtPoint(point)
// }
// }
// self.view.addGestureRecognizer(recognizer)
self.groupsButtonNode.highligthedChanged = { [weak self] highlighted in
if let self {
if highlighted {
self.groupsValueTextNode.layer.removeAnimation(forKey: "opacity")
self.groupsValueTextNode.alpha = 0.4
self.groupsAvatarsNode.layer.removeAnimation(forKey: "opacity")
self.groupsAvatarsNode.alpha = 0.4
self.groupsArrowNode.layer.removeAnimation(forKey: "opacity")
self.groupsArrowNode.alpha = 0.4
} else {
self.groupsValueTextNode.alpha = 1.0
self.groupsValueTextNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
self.groupsAvatarsNode.alpha = 1.0
self.groupsAvatarsNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
self.groupsArrowNode.alpha = 1.0
self.groupsArrowNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.groupsButtonNode.addTarget(self, action: #selector(self.groupsPressed), forControlEvents: .touchUpInside)
}
override public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
super.updateAbsoluteRect(rect, within: containerSize)
@ -207,6 +222,13 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
}
}
@objc private func groupsPressed() {
guard let item = self.item else {
return
}
item.controllerInteraction.openPeer(item.peer, .info(ChatControllerInteractionNavigateToPeer.InfoParams(switchToGroupsInCommon: true)), nil, .default)
}
public func asyncLayout() -> (_ item: ChatUserInfoItem, _ width: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
@ -214,17 +236,17 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
let makeRegistrationDateValueLayout = TextNode.asyncLayout(self.registrationDateValueTextNode)
let makePhoneCountryTitleLayout = TextNode.asyncLayout(self.phoneCountryTitleTextNode)
let makePhoneCountryValueLayout = TextNode.asyncLayout(self.phoneCountryValueTextNode)
let makeLocationCountryTitleLayout = TextNode.asyncLayout(self.locationCountryTitleTextNode)
let makeLocationCountryValueLayout = TextNode.asyncLayout(self.locationCountryValueTextNode)
let makeGroupsLayout = TextNode.asyncLayout(self.groupsTextNode)
let makeGroupsTitleLayout = TextNode.asyncLayout(self.groupsTitleTextNode)
let makeGroupsValueLayout = TextNode.asyncLayout(self.groupsValueTextNode)
let makeDisclaimerLayout = TextNodeWithEntities.asyncLayout(self.disclaimerTextNode)
let currentItem = self.item
let currentRegistrationDateText = self.registrationDateText
let currentPhoneCountryText = self.phoneCountryText
let currentLocationCountryText = self.locationCountryText
return { [weak self] item, params in
self?.item = item
let themeUpdated = item.presentationData.theme !== currentItem?.presentationData.theme
var backgroundSize = CGSize(width: 240.0, height: 0.0)
let verticalItemInset: CGFloat = 10.0
@ -239,13 +261,13 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
let subtitleColor = primaryTextColor.withAlphaComponent(item.presentationData.theme.theme.overallDarkAppearance ? 0.7 : 0.8)
backgroundSize.height += verticalInset
//TODO:localize
let constrainedWidth = params.width - (horizontalInset + horizontalContentInset) * 2.0
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder), font: Font.semibold(15.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += titleLayout.size.height
backgroundSize.height += verticalSpacing
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Not a contact", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_Subtitle, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += subtitleLayout.size.height
backgroundSize.height += verticalSpacing + paragraphSpacing
@ -254,6 +276,36 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
var maxTitleWidth: CGFloat = 0.0
var maxValueWidth: CGFloat = 0.0
var phoneCountryText: String?
let phoneCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let phoneCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let phoneCountry = item.phoneCountry {
if let currentPhoneCountryText {
phoneCountryText = currentPhoneCountryText
} else {
var countryName = ""
let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 }
if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) {
countryName = country.localizedName ?? country.name
} else if phoneCountry == "FT" {
countryName = item.presentationData.strings.Chat_NonContactUser_AnonymousNumber
} else if phoneCountry == "TS" {
countryName = "Test"
}
phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName
}
phoneCountryTitleLayoutAndApply = makePhoneCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_PhoneNumber, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
phoneCountryValueLayoutAndApply = makePhoneCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: phoneCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += phoneCountryValueLayoutAndApply?.0.size.height ?? 0
maxTitleWidth = max(maxTitleWidth, (phoneCountryTitleLayoutAndApply?.0.size.width ?? 0))
maxValueWidth = max(maxValueWidth, (phoneCountryValueLayoutAndApply?.0.size.width ?? 0))
} else {
phoneCountryTitleLayoutAndApply = nil
phoneCountryValueLayoutAndApply = nil
}
var registrationDateText: String?
let registrationDateTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let registrationDateValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
@ -270,7 +322,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
registrationDateText = ""
}
}
registrationDateTitleLayoutAndApply = makeRegistrationDateTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Registration", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
registrationDateTitleLayoutAndApply = makeRegistrationDateTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_Registration, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
registrationDateValueLayoutAndApply = makeRegistrationDateValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: registrationDateText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += registrationDateValueLayoutAndApply?.0.size.height ?? 0
@ -282,63 +334,57 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
registrationDateValueLayoutAndApply = nil
}
var phoneCountryText: String?
let phoneCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let phoneCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let phoneCountry = item.phoneCountry {
if let currentPhoneCountryText {
phoneCountryText = currentPhoneCountryText
} else {
var countryName = ""
let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 }
if let country = countriesConfiguration.countries.first(where: { $0.id == phoneCountry }) {
countryName = country.localizedName ?? country.name
}
phoneCountryText = emojiFlagForISOCountryCode(phoneCountry) + " " + countryName
}
phoneCountryTitleLayoutAndApply = makePhoneCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Phone Number", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
phoneCountryValueLayoutAndApply = makePhoneCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: phoneCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += phoneCountryValueLayoutAndApply?.0.size.height ?? 0
maxTitleWidth = max(maxTitleWidth, (phoneCountryTitleLayoutAndApply?.0.size.width ?? 0))
maxValueWidth = max(maxValueWidth, (phoneCountryValueLayoutAndApply?.0.size.width ?? 0))
let avatarImageSize: CGFloat = 18.0
let avatarSpacing: CGFloat = 9.0
let avatarBorder: CGFloat = 1.0
let groupsValueText: NSMutableAttributedString
let groupsInCommonCount = item.groupsInCommonCount
var estimatedValueOffset: CGFloat = 0.0
if groupsInCommonCount > 0 {
groupsValueText = NSMutableAttributedString(string: item.presentationData.strings.Chat_NonContactUser_GroupsCount(groupsInCommonCount), font: Font.semibold(13.0), textColor: primaryTextColor)
estimatedValueOffset = avatarImageSize + CGFloat(min(2, max(0, item.groupsInCommonCount - 1))) * avatarSpacing + 4.0
} else {
phoneCountryTitleLayoutAndApply = nil
phoneCountryValueLayoutAndApply = nil
groupsValueText = NSMutableAttributedString(string: "", font: Font.semibold(13.0), textColor: primaryTextColor)
}
let groupsTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let groupsValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if !groupsValueText.string.isEmpty {
groupsTitleLayoutAndApply = makeGroupsTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Chat_NonContactUser_Groups, font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
groupsValueLayoutAndApply = makeGroupsValueLayout(TextNodeLayoutArguments(attributedString: groupsValueText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += groupsValueLayoutAndApply?.0.size.height ?? 0.0
maxTitleWidth = max(maxTitleWidth, groupsTitleLayoutAndApply?.0.size.width ?? 0)
maxValueWidth = max(maxValueWidth, groupsValueLayoutAndApply?.0.size.width ?? 0 + estimatedValueOffset)
} else {
groupsTitleLayoutAndApply = nil
groupsValueLayoutAndApply = nil
}
backgroundSize.width = horizontalContentInset * 2.0 + maxTitleWidth + attributeSpacing + maxValueWidth
let disclaimerText: NSMutableAttributedString
if let verification = item.verification {
disclaimerText = NSMutableAttributedString(string: " # \(verification.description)", font: Font.regular(13.0), textColor: subtitleColor)
if let range = disclaimerText.string.range(of: "#") {
disclaimerText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: verification.iconFileId, file: nil), range: NSRange(range, in: disclaimerText.string))
disclaimerText.addAttribute(.foregroundColor, value: subtitleColor, range: NSRange(range, in: disclaimerText.string))
disclaimerText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: disclaimerText.string))
}
} else {
disclaimerText = NSMutableAttributedString(string: " # \(item.presentationData.strings.Chat_NonContactUser_Disclaimer)", font: Font.regular(13.0), textColor: subtitleColor)
if let range = disclaimerText.string.range(of: "#") {
disclaimerText.addAttribute(.attachment, value: PresentationResourcesChat.chatUserInfoWarningIcon(item.presentationData.theme.theme)!, range: NSRange(range, in: disclaimerText.string))
disclaimerText.addAttribute(.foregroundColor, value: subtitleColor, range: NSRange(range, in: disclaimerText.string))
disclaimerText.addAttribute(.baselineOffset, value: 2.0, range: NSRange(range, in: disclaimerText.string))
}
}
var locationCountryText: String?
let locationCountryTitleLayoutAndApply: (TextNodeLayout, () -> TextNode)?
let locationCountryValueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
if let locationCountry = item.locationCountry {
if let currentLocationCountryText {
locationCountryText = currentLocationCountryText
} else {
var countryName = ""
let countriesConfiguration = item.context.currentCountriesConfiguration.with { $0 }
if let country = countriesConfiguration.countries.first(where: { $0.id == locationCountry }) {
countryName = country.localizedName ?? country.name
}
locationCountryText = emojiFlagForISOCountryCode(locationCountry) + " " + countryName
}
locationCountryTitleLayoutAndApply = makeLocationCountryTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "Location", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
locationCountryValueLayoutAndApply = makeLocationCountryValueLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: locationCountryText ?? "", font: Font.semibold(13.0), textColor: primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: infoConstrainedSize, alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing
backgroundSize.height += locationCountryValueLayoutAndApply?.0.size.height ?? 0
maxTitleWidth = max(maxTitleWidth, (locationCountryTitleLayoutAndApply?.0.size.width ?? 0))
maxValueWidth = max(maxValueWidth, (locationCountryValueLayoutAndApply?.0.size.width ?? 0))
} else {
locationCountryTitleLayoutAndApply = nil
locationCountryValueLayoutAndApply = nil
}
backgroundSize.width = horizontalContentInset * 3.0 + maxTitleWidth + attributeSpacing + maxValueWidth
let (groupsLayout, groupsApply) = makeGroupsLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "No groups in common", font: Font.regular(13.0), textColor: subtitleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (disclaimerLayout, disclaimerApply) = makeDisclaimerLayout(TextNodeLayoutArguments(attributedString: disclaimerText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: backgroundSize.width - horizontalContentInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
backgroundSize.height += verticalSpacing * 2.0 + paragraphSpacing
backgroundSize.height += groupsLayout.size.height
backgroundSize.height += disclaimerLayout.size.height
backgroundSize.height += verticalInset
@ -347,25 +393,44 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: backgroundSize.height + verticalItemInset * 2.0), insets: UIEdgeInsets())
return (itemLayout, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.theme = item.presentationData.theme
if themeUpdated {
strongSelf.groupsArrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Contact List/SubtitleArrow"), color: primaryTextColor)
}
if item.groupsInCommonCount > 0 {
if strongSelf.groupsInCommonContext == nil {
let groupsInCommonContext = GroupsInCommonContext(account: item.context.account, peerId: item.peer.id)
strongSelf.groupsInCommonContext = groupsInCommonContext
strongSelf.groupsInCommonDisposable = (groupsInCommonContext.state
|> deliverOnMainQueue).start(next: { [weak self] state in
guard let self, let item = self.item else {
return
}
self.groupsInCommon = Array(state.peers.compactMap { $0.peer }.prefix(3))
self.groupsAvatarsNode.update(context: item.context, peers: self.groupsInCommon, synchronousLoad: true, imageSize: avatarImageSize, imageSpacing: avatarSpacing, borderWidth: avatarBorder)
})
}
}
if item.presentationData.theme.theme.overallDarkAppearance {
strongSelf.registrationDateTitleTextNode.layer.compositingFilter = nil
strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = nil
strongSelf.locationCountryTitleTextNode.layer.compositingFilter = nil
strongSelf.groupsTitleTextNode.layer.compositingFilter = nil
strongSelf.subtitleNode.layer.compositingFilter = nil
strongSelf.groupsTextNode.layer.compositingFilter = nil
strongSelf.disclaimerTextNode.textNode.layer.compositingFilter = nil
} else {
strongSelf.registrationDateTitleTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.phoneCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.locationCountryTitleTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.groupsTitleTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.subtitleNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.groupsTextNode.layer.compositingFilter = "overlayBlendMode"
strongSelf.disclaimerTextNode.textNode.layer.compositingFilter = "overlayBlendMode"
}
strongSelf.registrationDateText = registrationDateText
strongSelf.phoneCountryText = phoneCountryText
strongSelf.locationCountryText = locationCountryText
strongSelf.controllerInteraction = item.controllerInteraction
@ -387,25 +452,24 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
var attributeMidpoints: [CGFloat] = []
func appendAttributeMidpoint(titleLayout: TextNodeLayout?, valueLayout: TextNodeLayout?) {
if let titleLayout, let valueLayout {
let totalWidth = titleLayout.size.width + attributeSpacing + valueLayout.size.width
let titleOffset = titleLayout.size.width + attributeSpacing / 2.0
let midpoint = (backgroundSize.width - totalWidth) / 2.0 + titleOffset
if let valueLayout {
let midpoint = backgroundSize.width - horizontalContentInset - valueLayout.size.width - attributeSpacing / 2.0
attributeMidpoints.append(midpoint)
}
}
appendAttributeMidpoint(titleLayout: registrationDateTitleLayoutAndApply?.0, valueLayout: registrationDateValueLayoutAndApply?.0)
appendAttributeMidpoint(titleLayout: phoneCountryTitleLayoutAndApply?.0, valueLayout: phoneCountryValueLayoutAndApply?.0)
appendAttributeMidpoint(titleLayout: locationCountryTitleLayoutAndApply?.0, valueLayout: locationCountryValueLayoutAndApply?.0)
let middleX = floorToScreenPixels(attributeMidpoints.isEmpty ? backgroundSize.width / 2.0 : attributeMidpoints.reduce(0, +) / CGFloat(attributeMidpoints.count))
appendAttributeMidpoint(titleLayout: registrationDateTitleLayoutAndApply?.0, valueLayout: registrationDateValueLayoutAndApply?.0)
appendAttributeMidpoint(titleLayout: groupsTitleLayoutAndApply?.0, valueLayout: groupsValueLayoutAndApply?.0)
let middleX = floorToScreenPixels(attributeMidpoints.min() ?? backgroundSize.width / 2.0)
let titleMaxX: CGFloat = backgroundFrame.minX + middleX - attributeSpacing / 2.0
let valueMinX: CGFloat = backgroundFrame.minX + middleX + attributeSpacing / 2.0
func positionAttributeNodes(
titleTextNode: TextNode,
valueTextNode: TextNode,
valueOffset: CGFloat = 0.0,
titleLayoutAndApply: (TextNodeLayout, () -> TextNode)?,
valueLayoutAndApply: (TextNodeLayout, () -> TextNode)?
) {
@ -425,19 +489,13 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
}
let _ = valueApply()
valueTextNode.frame = CGRect(
origin: CGPoint(x: valueMinX, y: contentOriginY),
origin: CGPoint(x: valueMinX + valueOffset, y: contentOriginY),
size: valueLayout.size
)
contentOriginY += valueLayout.size.height + verticalSpacing
}
}
positionAttributeNodes(
titleTextNode: strongSelf.registrationDateTitleTextNode,
valueTextNode: strongSelf.registrationDateValueTextNode,
titleLayoutAndApply: registrationDateTitleLayoutAndApply,
valueLayoutAndApply: registrationDateValueLayoutAndApply
)
positionAttributeNodes(
titleTextNode: strongSelf.phoneCountryTitleTextNode,
valueTextNode: strongSelf.phoneCountryValueTextNode,
@ -445,16 +503,48 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
valueLayoutAndApply: phoneCountryValueLayoutAndApply
)
positionAttributeNodes(
titleTextNode: strongSelf.locationCountryTitleTextNode,
valueTextNode: strongSelf.locationCountryValueTextNode,
titleLayoutAndApply: locationCountryTitleLayoutAndApply,
valueLayoutAndApply: locationCountryValueLayoutAndApply
titleTextNode: strongSelf.registrationDateTitleTextNode,
valueTextNode: strongSelf.registrationDateValueTextNode,
titleLayoutAndApply: registrationDateTitleLayoutAndApply,
valueLayoutAndApply: registrationDateValueLayoutAndApply
)
var valueOffset: CGFloat = 0.0
if let groupsValueLayoutAndApply {
let avatarsFrame = CGRect(origin: CGPoint(x: valueMinX + groupsValueLayoutAndApply.0.size.width + 4.0, y: contentOriginY + floor((groupsValueLayoutAndApply.0.size.height - avatarImageSize) / 2.0)), size: CGSize(width: avatarImageSize + avatarSpacing * 2.0, height: avatarImageSize))
strongSelf.groupsAvatarsNode.frame = avatarsFrame
strongSelf.groupsAvatarsNode.updateLayout(size: avatarsFrame.size)
strongSelf.groupsAvatarsNode.update(context: item.context, peers: strongSelf.groupsInCommon, synchronousLoad: true, imageSize: avatarImageSize, imageSpacing: avatarSpacing, borderWidth: avatarBorder)
if groupsInCommonCount > 0 {
valueOffset = avatarImageSize + CGFloat(min(2, max(0, groupsInCommonCount - 1))) * avatarSpacing + 4.0
strongSelf.groupsButtonNode.frame = CGRect(origin: CGPoint(x: valueMinX, y: contentOriginY), size: CGSize(width: groupsValueLayoutAndApply.0.size.width + 20.0, height: 18.0))
strongSelf.groupsButtonNode.isHidden = false
strongSelf.groupsAvatarsNode.isHidden = false
strongSelf.groupsArrowNode.isHidden = false
if let icon = strongSelf.groupsArrowNode.image {
strongSelf.groupsArrowNode.frame = CGRect(origin: CGPoint(x: avatarsFrame.minX + valueOffset, y: contentOriginY + 4.0 - UIScreenPixel), size: icon.size)
}
} else {
strongSelf.groupsAvatarsNode.isHidden = true
strongSelf.groupsButtonNode.isHidden = true
strongSelf.groupsArrowNode.isHidden = true
}
}
positionAttributeNodes(
titleTextNode: strongSelf.groupsTitleTextNode,
valueTextNode: strongSelf.groupsValueTextNode,
titleLayoutAndApply: groupsTitleLayoutAndApply,
valueLayoutAndApply: groupsValueLayoutAndApply
)
contentOriginY += verticalSpacing + paragraphSpacing
let _ = groupsApply()
let groupsFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - groupsLayout.size.width) / 2.0), y: contentOriginY), size: groupsLayout.size)
strongSelf.groupsTextNode.frame = groupsFrame
let _ = disclaimerApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.context.animationCache, renderer: item.context.animationRenderer, placeholderColor: primaryTextColor.withMultipliedAlpha(0.4), attemptSynchronous: true))
let disclaimerFrame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + floor((backgroundSize.width - disclaimerLayout.size.width) / 2.0), y: contentOriginY), size: disclaimerLayout.size)
strongSelf.disclaimerTextNode.textNode.frame = disclaimerFrame
if strongSelf.backgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
backgroundContent.clipsToBounds = true
@ -502,88 +592,4 @@ public final class ChatUserInfoItemNode: ListViewItemNode {
let extra = self.offsetContainer.frame.contains(point)
return result || extra
}
// public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
// let textNodeFrame = self.textNode.frame
// if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - self.offsetContainer.frame.minX - textNodeFrame.minX, y: point.y - self.offsetContainer.frame.minY - textNodeFrame.minY)) {
// if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
// var concealed = true
// if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
// concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
// }
// return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
// } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
// return ChatMessageBubbleContentTapAction(content: .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false))
// } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
// return ChatMessageBubbleContentTapAction(content: .textMention(peerName))
// } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String {
// return ChatMessageBubbleContentTapAction(content: .botCommand(botCommand))
// } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
// return ChatMessageBubbleContentTapAction(content: .hashtag(hashtag.peerName, hashtag.hashtag))
// } else {
// return ChatMessageBubbleContentTapAction(content: .none)
// }
// } else {
// return ChatMessageBubbleContentTapAction(content: .none)
// }
// }
// @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
// switch recognizer.state {
// case .ended:
// if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
// switch gesture {
// case .tap:
// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
// switch tapAction.content {
// case .none, .ignore:
// break
// case let .url(url):
// self.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url.url, concealed: url.concealed, progress: tapAction.activate?()))
// case let .peerMention(peerId, _, _):
// if let item = self.item {
// let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
// |> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
// if let peer = peer {
// self?.item?.controllerInteraction.openPeer(peer, .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
// }
// })
// }
// case let .textMention(name):
// self.item?.controllerInteraction.openPeerMention(name, tapAction.activate?())
// case let .botCommand(command):
// self.item?.controllerInteraction.sendBotCommand(nil, command)
// case let .hashtag(peerName, hashtag):
// self.item?.controllerInteraction.openHashtag(peerName, hashtag)
// default:
// break
// }
// case .longTap, .doubleTap:
// if let item = self.item, self.backgroundNode.frame.contains(location) {
// let tapAction = self.tapActionAtPoint(location, gesture: gesture, isEstimating: false)
// switch tapAction.content {
// case .none, .ignore:
// break
// case let .url(url):
// item.controllerInteraction.longTap(.url(url.url), ChatControllerInteraction.LongTapParams())
// case let .peerMention(peerId, mention, _):
// item.controllerInteraction.longTap(.peerMention(peerId, mention), ChatControllerInteraction.LongTapParams())
// case let .textMention(name):
// item.controllerInteraction.longTap(.mention(name), ChatControllerInteraction.LongTapParams())
// case let .botCommand(command):
// item.controllerInteraction.longTap(.command(command), ChatControllerInteraction.LongTapParams())
// case let .hashtag(_, hashtag):
// item.controllerInteraction.longTap(.hashtag(hashtag), ChatControllerInteraction.LongTapParams())
// default:
// break
// }
// }
// default:
// break
// }
// }
// default:
// break
// }
// }
}

View file

@ -104,7 +104,11 @@ public final class GiftItemComponent: Component {
let isHidden: Bool
let isSoldOut: Bool
let isSelected: Bool
let isPinned: Bool
let isEditing: Bool
let mode: Mode
let action: (() -> Void)?
let contextAction: ((UIView, ContextGesture) -> Void)?
public init(
context: AccountContext,
@ -120,7 +124,11 @@ public final class GiftItemComponent: Component {
isHidden: Bool = false,
isSoldOut: Bool = false,
isSelected: Bool = false,
mode: Mode = .generic
isPinned: Bool = false,
isEditing: Bool = false,
mode: Mode = .generic,
action: (() -> Void)? = nil,
contextAction: ((UIView, ContextGesture) -> Void)? = nil
) {
self.context = context
self.theme = theme
@ -135,7 +143,11 @@ public final class GiftItemComponent: Component {
self.isHidden = isHidden
self.isSoldOut = isSoldOut
self.isSelected = isSelected
self.isPinned = isPinned
self.isEditing = isEditing
self.mode = mode
self.action = action
self.contextAction = contextAction
}
public static func ==(lhs: GiftItemComponent, rhs: GiftItemComponent) -> Bool {
@ -178,16 +190,27 @@ public final class GiftItemComponent: Component {
if lhs.isSelected != rhs.isSelected {
return false
}
if lhs.isPinned != rhs.isPinned {
return false
}
if lhs.isEditing != rhs.isEditing {
return false
}
if lhs.mode != rhs.mode {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
return true
}
public final class View: UIView {
public final class View: ContextControllerSourceView {
private var component: GiftItemComponent?
private weak var componentState: EmptyComponentState?
private let containerButton = HighlightTrackingButton()
private let backgroundLayer = SimpleLayer()
private var loadingBackground: ComponentView<Empty>?
@ -207,8 +230,9 @@ public final class GiftItemComponent: Component {
private var disposables = DisposableSet()
private var fetchedFiles = Set<Int64>()
private var hiddenIconBackground: UIVisualEffectView?
private var iconBackground: UIVisualEffectView?
private var hiddenIcon: UIImageView?
private var pinnedIcon: UIImageView?
override init(frame: CGRect) {
super.init(frame: frame)
@ -219,6 +243,16 @@ public final class GiftItemComponent: Component {
self.backgroundLayer.cornerCurve = .circular
}
self.backgroundLayer.masksToBounds = true
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
component.contextAction?(self, gesture)
}
self.containerButton.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
@ -229,12 +263,18 @@ public final class GiftItemComponent: Component {
self.disposables.dispose()
}
@objc private func buttonPressed() {
self.component?.action?()
}
func update(component: GiftItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let isFirstTime = self.component == nil
let previousComponent = self.component
self.component = component
self.componentState = state
self.isGestureEnabled = component.contextAction != nil
var themeUpdated = false
if previousComponent?.theme !== component.theme {
themeUpdated = true
@ -590,7 +630,7 @@ public final class GiftItemComponent: Component {
}
}
if let peer = component.peer {
if let peer = component.peer, !component.isPinned {
let avatarNode: AvatarNode
if let current = self.avatarNode {
avatarNode = current
@ -608,6 +648,9 @@ public final class GiftItemComponent: Component {
}
avatarNode.frame = CGRect(origin: CGPoint(x: 5.0, y: 5.0), size: CGSize(width: 20.0, height: 20.0))
} else if let avatarNode = self.avatarNode {
self.avatarNode = nil
avatarNode.view.removeFromSuperview()
}
if let backgroundColor, let _ = secondBackgroundColor {
@ -617,50 +660,110 @@ public final class GiftItemComponent: Component {
}
transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: size))
transition.setFrame(view: self.containerButton, frame: CGRect(origin: .zero, size: size))
if component.isHidden {
let hiddenIconBackground: UIVisualEffectView
let hiddenIcon: UIImageView
if let currentBackground = self.hiddenIconBackground, let currentIcon = self.hiddenIcon {
hiddenIconBackground = currentBackground
hiddenIcon = currentIcon
var iconBackgroundSize: CGSize?
if component.isEditing {
if !component.isPinned && backgroundColor != nil {
iconBackgroundSize = CGSize(width: 48.0, height: 48.0)
}
} else {
if component.isHidden {
iconBackgroundSize = CGSize(width: 30.0, height: 30.0)
}
}
if let iconBackgroundSize {
let iconBackground: UIVisualEffectView
var iconBackgroundTransition = transition
if let currentBackground = self.iconBackground {
iconBackground = currentBackground
} else {
iconBackgroundTransition = .immediate
let blurEffect: UIBlurEffect
if #available(iOS 13.0, *) {
blurEffect = UIBlurEffect(style: .systemThinMaterialDark)
} else {
blurEffect = UIBlurEffect(style: .dark)
}
hiddenIconBackground = UIVisualEffectView(effect: blurEffect)
hiddenIconBackground.clipsToBounds = true
hiddenIconBackground.layer.cornerRadius = 15.0
self.hiddenIconBackground = hiddenIconBackground
iconBackground = UIVisualEffectView(effect: blurEffect)
iconBackground.clipsToBounds = true
self.iconBackground = iconBackground
hiddenIcon = UIImageView(image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HiddenIcon"), color: .white))
self.hiddenIcon = hiddenIcon
self.addSubview(hiddenIconBackground)
hiddenIconBackground.contentView.addSubview(hiddenIcon)
self.addSubview(iconBackground)
if !isFirstTime {
hiddenIconBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
hiddenIconBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
iconBackground.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
iconBackground.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
iconBackgroundTransition.containedViewLayoutTransition.animateView {
iconBackground.frame = iconBackgroundSize.centered(around: animationFrame.center)
iconBackground.layer.cornerRadius = iconBackgroundSize.width / 2.0
}
} else if let iconBackground = self.iconBackground {
self.iconBackground = nil
iconBackground.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
iconBackground.removeFromSuperview()
})
iconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if component.isPinned || (component.isEditing && backgroundColor != nil) {
let pinnedIcon: UIImageView
if let currentIcon = self.pinnedIcon {
pinnedIcon = currentIcon
} else {
pinnedIcon = UIImageView(image: UIImage(bundleImageName: !component.isPinned ? "Peer Info/PinnedLargeIcon" : "Peer Info/PinnedIcon")?.withRenderingMode(.alwaysTemplate))
self.pinnedIcon = pinnedIcon
self.addSubview(pinnedIcon)
if !isFirstTime {
pinnedIcon.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
pinnedIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
if component.isPinned {
pinnedIcon.frame = CGRect(origin: CGPoint(x: 3.0, y: 3.0), size: CGSize(width: 24.0, height: 24.0))
pinnedIcon.tintColor = backgroundColor == nil ? component.theme.list.itemSecondaryTextColor : .white
} else {
let iconSize = CGSize(width: 48.0, height: 48.0)
pinnedIcon.frame = iconSize.centered(around: animationFrame.center)
pinnedIcon.tintColor = .white
}
} else if let pinnedIcon = self.pinnedIcon {
self.pinnedIcon = nil
pinnedIcon.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
pinnedIcon.removeFromSuperview()
})
pinnedIcon.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if component.isHidden && !component.isEditing {
let hiddenIcon: UIImageView
if let currentIcon = self.hiddenIcon {
hiddenIcon = currentIcon
} else {
hiddenIcon = UIImageView(image: generateTintedImage(image: UIImage(bundleImageName: "Peer Info/HiddenIcon"), color: .white))
self.hiddenIcon = hiddenIcon
self.addSubview(hiddenIcon)
if !isFirstTime {
hiddenIcon.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
hiddenIcon.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let iconSize = CGSize(width: 30.0, height: 30.0)
hiddenIconBackground.frame = iconSize.centered(around: animationFrame.center)
hiddenIcon.frame = CGRect(origin: .zero, size: iconSize)
} else {
if let hiddenIconBackground = self.hiddenIconBackground {
self.hiddenIconBackground = nil
self.hiddenIcon = nil
hiddenIconBackground.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
hiddenIconBackground.removeFromSuperview()
})
hiddenIconBackground.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
hiddenIcon.frame = iconSize.centered(around: animationFrame.center)
} else if let hiddenIcon = self.hiddenIcon {
self.hiddenIcon = nil
hiddenIcon.layer.animateAlpha(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false, completion: { _ in
hiddenIcon.removeFromSuperview()
})
hiddenIcon.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
}
if case .grid = component.mode {
@ -700,6 +803,13 @@ public final class GiftItemComponent: Component {
}
}
if let _ = component.action {
self.addSubview(self.containerButton)
self.containerButton.isUserInteractionEnabled = true
} else {
self.containerButton.isUserInteractionEnabled = false
}
return size
}
}

View file

@ -246,16 +246,6 @@ final class ChatGiftPreviewItemNode: ListViewItemNode {
let message = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
if let chargeStars = item.chargeStars {
let media = [
TelegramMediaAction(
action: .customText(text: "You paid \(chargeStars) Stars to send a message", entities: [MessageTextEntity(range: 9 ..< 18, type: .Bold)], additionalAttributes: nil)
)
]
let message = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: chatPeerId, namespace: 0, id: 1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 66000, flags: [.Incoming], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peers[authorPeerId], text: "", attributes: [], media: media, peers: peers, associatedMessages: messages, associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
items.append(item.context.sharedContext.makeChatMessagePreviewItem(context: item.context, messages: [message], theme: item.componentTheme, strings: item.strings, wallpaper: item.wallpaper, fontSize: item.fontSize, chatBubbleCorners: item.chatBubbleCorners, dateTimeFormat: item.dateTimeFormat, nameOrder: item.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: currentBackgroundNode, availableReactions: nil, accountPeer: nil, isCentered: false, isPreview: true, isStandalone: false))
}
}
var nodes: [ListViewItemNode] = []

View file

@ -223,21 +223,23 @@ final class GiftSetupScreenComponent: Component {
guard let component = self.component, let environment = self.environment else {
return
}
switch component.subject {
case let .premium(product):
if self.payWithStars, let starsPrice = product.starsPrice, let peer = self.peerMap[component.peerId] {
//TODO:localize
if let balance = component.context.starsContext?.currentState?.balance, balance.value < starsPrice {
self.proceedWithStarGift()
} else {
let priceString = presentationStringsFormattedNumber(Int32(starsPrice), environment.dateTimeFormat.groupingSeparator)
let controller = textAlertController(
context: component.context,
title: "Send a Gift",
text: "Are you sure you want to gift **Telegram Premium** to \(peer.compactDisplayTitle) for **\(priceString) Stars**?",
title: environment.strings.Gift_Send_Premium_Confirmation_Title,
text: environment.strings.Gift_Send_Premium_Confirmation_Text(
peer.compactDisplayTitle,
environment.strings.Gift_Send_Premium_Confirmation_Text_Stars(Int32(starsPrice))
).string,
actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {}),
TextAlertAction(type: .defaultAction, title: "Confirm", action: { [weak self] in
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: environment.strings.Gift_Send_Premium_Confirmation_Confirm, action: { [weak self] in
if let self {
self.proceedWithStarGift()
}

View file

@ -111,6 +111,8 @@ private extension MessageInputActionButtonComponent.Mode {
return ButtonIcon(icon: .apply)
case .send:
return ButtonIcon(icon: .send)
case .stars:
return nil
default:
return nil
}
@ -121,6 +123,7 @@ public final class MessageInputActionButtonComponent: Component {
public enum Mode: Equatable {
case none
case send
case stars(Int64)
case apply
case voiceInput
case videoInput
@ -472,7 +475,7 @@ public final class MessageInputActionButtonComponent: Component {
break
case .captionUp, .captionDown:
sendAlpha = 0.0
case .send, .apply, .attach, .delete, .forward, .removeVideoInput, .repost:
case .send, .apply, .attach, .delete, .forward, .removeVideoInput, .repost, .stars:
sendAlpha = 1.0
case let .like(reaction, _, _):
if reaction != nil {
@ -671,7 +674,7 @@ public final class MessageInputActionButtonComponent: Component {
if previousComponent?.mode != component.mode {
switch component.mode {
case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more, .like, .repost, .captionUp, .captionDown:
case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more, .like, .repost, .captionUp, .captionDown, .stars:
micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate)
case .videoInput, .removeVideoInput:
micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate)

View file

@ -1452,7 +1452,11 @@ public final class MessageInputPanelComponent: Component {
inputActionButtonMode = .send
} else {
if self.textFieldExternalState.hasText {
inputActionButtonMode = .send
if let sendPaidMessageStars = component.sendPaidMessageStars {
inputActionButtonMode = .stars(sendPaidMessageStars.value)
} else {
inputActionButtonMode = .send
}
} else if !isEditing && component.forwardAction != nil {
inputActionButtonMode = .forward
} else {
@ -1477,7 +1481,7 @@ public final class MessageInputPanelComponent: Component {
switch mode {
case .none:
break
case .send:
case .send, .stars:
if case .up = action {
if component.recordedAudioPreview != nil {
component.sendMessageAction()

View file

@ -1859,6 +1859,21 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasSavedMessageTags = .single(false)
}
let starsRevenueContextAndState = context.engine.data.subscribe(
TelegramEngine.EngineData.Item.Peer.CanViewStarsRevenue(id: peerId)
)
|> distinctUntilChanged
|> mapToSignal { canViewStarsRevenue -> Signal<(StarsRevenueStatsContext?, StarsRevenueStats?), NoError> in
guard canViewStarsRevenue else {
return .single((nil, nil))
}
let starsRevenueStatsContext = StarsRevenueStatsContext(account: context.account, peerId: peerId)
return starsRevenueStatsContext.state
|> map { state -> (StarsRevenueStatsContext?, StarsRevenueStats?) in
return (starsRevenueStatsContext, state.stats)
}
}
let isPremiumRequiredForStoryPosting: Signal<Bool, NoError> = isPremiumRequiredForStoryPosting(context: context)
return combineLatest(queue: .mainQueue(),
@ -1878,9 +1893,10 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
hasSavedMessages,
hasSavedMessagesChats,
hasSavedMessageTags,
isPremiumRequiredForStoryPosting
isPremiumRequiredForStoryPosting,
starsRevenueContextAndState
)
|> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting -> Signal<PeerInfoScreenData, NoError> in
|> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState -> Signal<PeerInfoScreenData, NoError> in
var discussionPeer: Peer?
if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
discussionPeer = peer
@ -1980,8 +1996,8 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
isPremiumRequiredForStoryPosting: isPremiumRequiredForStoryPosting,
personalChannel: nil,
starsState: nil,
starsRevenueStatsState: nil,
starsRevenueStatsContext: nil,
starsRevenueStatsState: starsRevenueContextAndState.1,
starsRevenueStatsContext: starsRevenueContextAndState.0,
revenueStatsState: nil,
revenueStatsContext: nil,
profileGiftsContext: nil,

View file

@ -43,9 +43,11 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
private let titleNode: ImmediateTextNode
private let buttonNode: HighlightTrackingButtonNode
private var iconLayers: [InlineStickerItemLayer] = []
private var iconLayers: [AnyHashable: InlineStickerItemLayer] = [:]
private var isSelected: Bool = false
private var icons: [ProfileGiftsContext.State.StarGift] = []
private var titleWidth: CGFloat?
init(pressed: @escaping () -> Void) {
self.pressed = pressed
@ -67,27 +69,53 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
self.pressed()
}
func updateText(context: AccountContext, title: String, icons: [TelegramMediaFile] = [], isSelected: Bool, presentationData: PresentationData) {
func updateText(context: AccountContext, title: String, icons: [ProfileGiftsContext.State.StarGift] = [], isSelected: Bool, presentationData: PresentationData) {
self.isSelected = isSelected
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor)
self.icons = icons
if !icons.isEmpty {
if self.iconLayers.isEmpty {
for icon in icons {
let iconSize = CGSize(width: 18.0, height: 18.0)
var validIds = Set<AnyHashable>()
var index = 0
for icon in icons {
let id: AnyHashable
if let reference = icon.reference {
id = reference
} else {
id = index
}
validIds.insert(id)
let iconSize = CGSize(width: 18.0, height: 18.0)
if let _ = self.iconLayers[id] {
} else {
var file: TelegramMediaFile?
switch icon.gift {
case let .generic(gift):
file = gift.file
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, fileValue, _) = attribute {
file = fileValue
}
}
}
guard let file else {
continue
}
let emoji = ChatTextInputTextCustomEmojiAttribute(
interactivelySelectedFromPackId: nil,
fileId: icon.fileId.id,
file: icon
fileId: file.fileId.id,
file: file
)
let animationLayer = InlineStickerItemLayer(
context: .account(context),
userLocation: .other,
attemptSynchronousLoad: false,
emoji: emoji,
file: icon,
file: file,
cache: context.animationCache,
renderer: context.animationRenderer,
unique: true,
@ -96,12 +124,30 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
loopCount: 1
)
animationLayer.isVisibleForAnimations = true
self.iconLayers.append(animationLayer)
self.iconLayers[id] = animationLayer
self.layer.addSublayer(animationLayer)
animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2)
}
index += 1
}
var removeIds: [AnyHashable] = []
for (id, layer) in self.iconLayers {
if !validIds.contains(id) {
removeIds.append(id)
layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
layer.removeFromSuperlayer()
})
}
}
for id in removeIds {
self.iconLayers.removeValue(forKey: id)
}
} else {
for layer in self.iconLayers {
for (_, layer) in self.iconLayers {
layer.removeFromSuperlayer()
}
self.iconLayers.removeAll()
@ -115,24 +161,54 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
}
func updateLayout(height: CGFloat) -> CGFloat {
var totalWidth: CGFloat = 0.0
let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude))
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
totalWidth = titleSize.width
let iconSize = CGSize(width: 18.0, height: 18.0)
let spacing: CGFloat = 1.0
self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
self.titleWidth = titleSize.width
var totalWidth = titleSize.width
if !self.iconLayers.isEmpty {
totalWidth += 2.0
let iconSize = CGSize(width: 18.0, height: 18.0)
let spacing: CGFloat = 1.0
for iconlayer in self.iconLayers {
iconlayer.frame = CGRect(origin: CGPoint(x: totalWidth, y: 15.0), size: iconSize)
totalWidth += iconSize.width + spacing
}
totalWidth += (iconSize.width + spacing) * CGFloat(self.iconLayers.count)
totalWidth -= spacing
}
self.layoutIcons(transition: .animated(duration: 0.3, curve: .spring))
return totalWidth
}
func layoutIcons(transition: ContainedViewLayoutTransition) {
guard let titleWidth = self.titleWidth else {
return
}
let iconSize = CGSize(width: 18.0, height: 18.0)
let spacing: CGFloat = 1.0
var origin = CGPoint(x: titleWidth + 2.0, y: 15.0)
var index = 0
for icon in self.icons {
let id: AnyHashable
if let reference = icon.reference {
id = reference
} else {
id = index
}
if let layer = self.iconLayers[id] {
var iconTransition = transition
if layer.frame.width.isZero {
iconTransition = .immediate
}
iconTransition.updateFrame(layer: layer, frame: CGRect(origin: origin, size: iconSize))
}
origin.x += iconSize.width + spacing
index += 1
}
}
func updateArea(size: CGSize, sideInset: CGFloat) {
self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height))
}
@ -141,7 +217,7 @@ final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode {
struct PeerInfoPaneSpecifier: Equatable {
var key: PeerInfoPaneKey
var title: String
var icons: [TelegramMediaFile]
var icons: [ProfileGiftsContext.State.StarGift]
}
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
@ -471,7 +547,7 @@ private final class PeerInfoPendingPane {
}
}
}
paneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, profileGifts: data.profileGiftsContext!, canManage: canManage)
paneNode = PeerInfoGiftsPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, profileGifts: data.profileGiftsContext!, canManage: canManage)
case .stories, .storyArchive, .botPreview:
var canManage = false
if let peer = data.peer {
@ -884,11 +960,15 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
for (_, pane) in self.currentPanes {
if let paneNode = pane.node as? PeerInfoStoryPaneNode {
paneNode.updateIsReordering(isReordering: isReordering, animated: animated)
} else if let paneNode = pane.node as? PeerInfoGiftsPaneNode {
paneNode.updateIsReordering(isReordering: isReordering, animated: animated)
}
}
for (_, pane) in self.pendingPanes {
if let paneNode = pane.pane.node as? PeerInfoStoryPaneNode {
paneNode.updateIsReordering(isReordering: isReordering, animated: false)
} else if let paneNode = pane.pane.node as? PeerInfoGiftsPaneNode {
paneNode.updateIsReordering(isReordering: isReordering, animated: animated)
}
}
}
@ -1185,7 +1265,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
self.tabsContainerNode.update(size: CGSize(width: size.width - sideInset * 2.0, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in
let title: String
var icons: [TelegramMediaFile] = []
var icons: [ProfileGiftsContext.State.StarGift] = []
switch key {
case .stories:
title = presentationData.strings.PeerInfo_PaneStories
@ -1219,19 +1299,9 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
title = presentationData.strings.PeerInfo_SavedMessagesTabTitle
case .gifts:
title = presentationData.strings.PeerInfo_PaneGifts
icons = data?.profileGiftsContext?.currentState?.gifts.prefix(3).compactMap { gift in
switch gift.gift {
case let .generic(gift):
return gift.file
case let .unique(gift):
for attribute in gift.attributes {
if case let .model(_, file, _) = attribute {
return file
}
}
return nil
}
} ?? []
if let gifts = data?.profileGiftsContext?.currentState?.gifts.prefix(3) {
icons = Array(gifts)
}
}
return PeerInfoPaneSpecifier(key: key, title: title, icons: icons)
}, selectedPane: self.currentPaneKey, disableSwitching: disableTabSwitching, transitionFraction: self.transitionFraction, transition: transition)

View file

@ -1233,6 +1233,7 @@ private enum InfoSection: Int, CaseIterable {
case balances
case permissions
case peerInfoTrailing
case peerSettings
case peerMembers
case botAffiliateProgram
}
@ -1854,53 +1855,68 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
interaction.openParticipantsSection(.memberRequests)
}))
}
}
}
}
if channel.adminRights != nil || channel.flags.contains(.isCreator) {
let section: InfoSection
if case .group = channel.info {
section = .peerSettings
} else {
section = .peerMembers
}
if cachedData.flags.contains(.canViewRevenue) || cachedData.flags.contains(.canViewStarsRevenue) {
let revenueBalance = data.revenueStatsState?.balances.currentBalance ?? 0
let starsBalance = data.starsRevenueStatsState?.balances.currentBalance ?? StarsAmount.zero
let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0
let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? StarsAmount.zero
if overallRevenueBalance > 0 || overallStarsBalance > StarsAmount.zero {
let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0))
let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
let labelColor = presentationData.theme.list.itemSecondaryTextColor
if cachedData.flags.contains(.canViewRevenue) || cachedData.flags.contains(.canViewStarsRevenue) {
let revenueBalance = data.revenueStatsState?.balances.currentBalance ?? 0
let starsBalance = data.starsRevenueStatsState?.balances.currentBalance ?? StarsAmount.zero
let overallRevenueBalance = data.revenueStatsState?.balances.overallRevenue ?? 0
let overallStarsBalance = data.starsRevenueStatsState?.balances.overallRevenue ?? StarsAmount.zero
if overallRevenueBalance > 0 || overallStarsBalance > StarsAmount.zero {
let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0))
let labelFont = Font.regular(presentationData.listsFontSize.itemListBaseFontSize)
let labelColor = presentationData.theme.list.itemSecondaryTextColor
let attributedString = NSMutableAttributedString()
if overallRevenueBalance > 0 {
attributedString.append(NSAttributedString(string: "#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))", font: labelFont, textColor: labelColor))
}
if overallStarsBalance > StarsAmount.zero {
if !attributedString.string.isEmpty {
attributedString.append(NSAttributedString(string: " ", font: labelFont, textColor: labelColor))
}
attributedString.append(NSAttributedString(string: "*", font: labelFont, textColor: labelColor))
let formattedLabel = formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)
let starsAttributedString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
attributedString.append(starsAttributedString)
}
if let range = attributedString.string.range(of: "#") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 1, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemBalance, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: {
interaction.openStats(.monetization)
}))
let attributedString = NSMutableAttributedString()
if overallRevenueBalance > 0 {
attributedString.append(NSAttributedString(string: "#\(formatTonAmountText(revenueBalance, dateTimeFormat: presentationData.dateTimeFormat))", font: labelFont, textColor: labelColor))
}
if overallStarsBalance > StarsAmount.zero {
if !attributedString.string.isEmpty {
attributedString.append(NSAttributedString(string: " ", font: labelFont, textColor: labelColor))
}
attributedString.append(NSAttributedString(string: "*", font: labelFont, textColor: labelColor))
let formattedLabel = formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)
let starsAttributedString = tonAmountAttributedString(formattedLabel, integralFont: labelFont, fractionalFont: smallLabelFont, color: labelColor, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator).mutableCopy() as! NSMutableAttributedString
attributedString.append(starsAttributedString)
}
if let range = attributedString.string.range(of: "#") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .ton), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
if let range = attributedString.string.range(of: "*") {
attributedString.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 1, file: nil, custom: .stars(tinted: false)), range: NSRange(range, in: attributedString.string))
attributedString.addAttribute(.baselineOffset, value: 1.5, range: NSRange(range, in: attributedString.string))
}
items[.peerMembers]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: presentationData.strings.Channel_Info_Settings, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
interaction.openEditing()
items[section]!.append(PeerInfoScreenDisclosureItem(id: ItemBalance, label: .attributedText(attributedString), text: presentationData.strings.PeerInfo_Bot_Balance, icon: PresentationResourcesSettings.balance, action: {
interaction.openStats(.monetization)
}))
}
}
let settingsTitle: String
switch channel.info {
case .broadcast:
settingsTitle = presentationData.strings.Channel_Info_Settings
case .group:
settingsTitle = presentationData.strings.Group_Info_Settings
}
items[section]!.append(PeerInfoScreenDisclosureItem(id: ItemEdit, label: .none, text: settingsTitle, icon: UIImage(bundleImageName: "Chat/Info/SettingsIcon"), action: {
interaction.openEditing()
}))
}
}
}
@ -8282,7 +8298,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let statsController: ViewController
if let channel = peer as? TelegramChannel, case .group = channel.info {
statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)
if case .monetization = section {
statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: section, existingStarsRevenueContext: data.starsRevenueStatsContext, boostStatus: boostStatus)
} else {
statsController = groupStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id)
}
} else {
statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: section, boostStatus: boostStatus)
}
@ -10951,6 +10971,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let currentPaneKey = self.paneContainerNode.currentPaneKey, case .gifts = currentPaneKey else {
return
}
guard let pane = self.paneContainerNode.currentPane?.node as? PeerInfoGiftsPaneNode else {
return
}
guard let controller = self.controller else {
return
}
@ -10968,14 +10991,22 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let strings = self.presentationData.strings
let items: Signal<ContextController.Items, NoError> = giftsContext.state
|> map { state in
return (state.filter, state.sorting)
var hasPinnedGifts = false
for gift in state.gifts {
if gift.pinnedToTop {
hasPinnedGifts = true
break
}
}
return (state.filter, state.sorting, hasPinnedGifts)
}
|> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in
let filterEquals = lhs.0 == rhs.0
let sortingEquals = lhs.1 == rhs.1
return filterEquals && sortingEquals
let hasPinnedGiftsEquals = lhs.2 == rhs.2
return filterEquals && sortingEquals && hasPinnedGiftsEquals
})
|> map { [weak giftsContext] filter, sorting -> ContextController.Items in
|> map { [weak giftsContext] filter, sorting, hasPinnedGifts -> ContextController.Items in
var items: [ContextMenuItem] = []
items.append(.action(ContextMenuActionItem(text: sorting == .date ? strings.PeerInfo_Gifts_SortByValue : strings.PeerInfo_Gifts_SortByDate, icon: { theme in
@ -10986,6 +11017,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
giftsContext?.updateSorting(sorting == .date ? .value : .date)
})))
if hasPinnedGifts {
items.append(.action(ContextMenuActionItem(text: strings.PeerInfo_Gifts_Reorder, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReorderItems"), color: theme.contextMenu.primaryColor)
}, action: { _, f in
f(.default)
pane.beginReordering()
})))
}
items.append(.separator)
let toggleFilter: (ProfileGiftsContext.Filters) -> Void = { [weak giftsContext] value in
@ -12499,6 +12540,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
fileprivate let starsContext: StarsContext?
private let switchToRecommendedChannels: Bool
private let switchToGifts: Bool
private let switchToGroupsInCommon: Bool
let chatLocation: ChatLocation
private let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
@ -12555,7 +12597,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
private var validLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false, switchToGifts: Bool = false) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peerId: PeerId, avatarInitiallyExpanded: Bool, isOpenedFromChat: Bool, nearbyPeerDistance: Int32?, reactionSourceMessageId: MessageId?, callMessages: [Message], isSettings: Bool = false, isMyProfile: Bool = false, hintGroupInCommon: PeerId? = nil, requestsContext: PeerInvitationImportersContext? = nil, forumTopicThread: ChatReplyThreadMessage? = nil, switchToRecommendedChannels: Bool = false, switchToGifts: Bool = false, switchToGroupsInCommon: Bool = false) {
self.context = context
self.updatedPresentationData = updatedPresentationData
self.peerId = peerId
@ -12570,6 +12612,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
self.requestsContext = requestsContext
self.switchToRecommendedChannels = switchToRecommendedChannels
self.switchToGifts = switchToGifts
self.switchToGroupsInCommon = switchToGroupsInCommon
if let forumTopicThread = forumTopicThread {
self.chatLocation = .replyThread(message: forumTopicThread)
@ -12915,6 +12958,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
initialPaneKey = .similarChannels
} else if self.switchToGifts {
initialPaneKey = .gifts
} else if self.switchToGroupsInCommon {
initialPaneKey = .groupsInCommon
}
self.displayNode = PeerInfoScreenNode(controller: self, context: self.context, peerId: self.peerId, avatarInitiallyExpanded: self.avatarInitiallyExpanded, isOpenedFromChat: self.isOpenedFromChat, nearbyPeerDistance: self.nearbyPeerDistance, reactionSourceMessageId: self.reactionSourceMessageId, callMessages: self.callMessages, isSettings: self.isSettings, isMyProfile: self.isMyProfile, hintGroupInCommon: self.hintGroupInCommon, requestsContext: self.requestsContext, starsContext: self.starsContext, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, initialPaneKey: initialPaneKey)
self.controllerNode.accountsAndPeers.set(self.accountsAndPeers.get() |> map { $0.1 })

View file

@ -0,0 +1,276 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import GiftAnimationComponent
private final class GiftContextPreviewComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let gift: ProfileGiftsContext.State.StarGift
init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift
) {
self.context = context
self.gift = gift
}
static func ==(lhs: GiftContextPreviewComponent, rhs: GiftContextPreviewComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.gift != rhs.gift {
return false
}
return true
}
public final class View: UIView {
private let animation = ComponentView<Empty>()
private let giftCompositionExternalState = GiftCompositionComponent.ExternalState()
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var component: GiftContextPreviewComponent?
private weak var state: EmptyComponentState?
private var environment: ViewControllerComponentContainer.Environment?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: GiftContextPreviewComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
self.component = component
self.state = state
let subject: GiftCompositionComponent.Subject
switch component.gift.gift {
case let .generic(gift):
subject = .generic(gift.file)
case let .unique(gift):
subject = .unique(gift)
}
let animationSize = self.animation.update(
transition: .immediate,
component: AnyComponent(GiftCompositionComponent(
context: component.context,
theme: environment.theme,
subject: subject,
animationOffset: nil,
animationScale: nil,
displayAnimationStars: false,
externalState: self.giftCompositionExternalState,
requestUpdate: { [weak state] in
state?.updated()
}
)),
environment: {},
containerSize: availableSize
)
if let view = self.animation.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: .zero, size: animationSize)
}
if case let .unique(uniqueGift) = component.gift.gift {
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(
NSAttributedString(string: uniqueGift.title, font: Font.semibold(20.0), textColor: .white)
))),
environment: {},
containerSize: availableSize
)
if let view = self.title.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 40.0), size: titleSize)
}
let vibrantColor: UIColor
if let previewPatternColor = giftCompositionExternalState.previewPatternColor {
vibrantColor = previewPatternColor.withMultiplied(hue: 1.0, saturation: 1.02, brightness: 1.25).mixedWith(UIColor.white, alpha: 0.3)
} else {
vibrantColor = UIColor.white.withAlphaComponent(0.6)
}
let subtitleSize = self.subtitle.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(text: .plain(
NSAttributedString(string: "\(environment.strings.Gift_Unique_Collectible) #\(uniqueGift.number)", font: Font.regular(13.0), textColor: vibrantColor)
))),
environment: {},
containerSize: availableSize
)
if let view = self.subtitle.view {
if view.superview == nil {
self.addSubview(view)
}
view.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - subtitleSize.width) / 2.0), y: availableSize.height - subtitleSize.height - 20.0), size: subtitleSize)
}
}
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class GiftContextPreviewController: ViewController {
fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate {
private weak var controller: GiftContextPreviewController?
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
init(controller: GiftContextPreviewController) {
self.controller = controller
self.presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
self.backgroundColor = .clear
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveModalDismiss = true
self.view.disablesInteractiveKeyboardGestureRecognizer = true
}
func requestLayout(transition: ComponentTransition) {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) {
guard let controller = self.controller else {
return
}
self.validLayout = layout
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: 0.0,
navigationHeight: 0.0,
safeInsets: layout.safeInsets,
additionalInsets: layout.additionalInsets,
inputHeight: 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: nil,
isVisible: true,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(
GiftContextPreviewComponent(
context: controller.context,
gift: controller.gift
)
),
environment: {
environment
},
forceUpdate: forceUpdate,
containerSize: layout.size
)
if let componentView = self.componentHost.view {
if componentView.superview == nil {
componentView.clipsToBounds = true
self.view.addSubview(componentView)
}
let componentFrame = CGRect(origin: .zero, size: componentSize)
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
}
}
}
fileprivate var node: Node {
return self.displayNode as! Node
}
fileprivate let context: AccountContext
fileprivate let gift: ProfileGiftsContext.State.StarGift
init(
context: AccountContext,
gift: ProfileGiftsContext.State.StarGift
) {
self.context = context
self.gift = gift
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
}
override func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
let minSide = min(layout.size.width, layout.size.height)
if case .unique = self.gift.gift {
return CGSize(width: minSide, height: floor(minSide * 0.66))
} else {
return CGSize(width: minSide, height: 180.0)
}
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition))
}
}

View file

@ -62,11 +62,11 @@ public final class StarsBalanceOverlayComponent: Component {
func update(component: StarsBalanceOverlayComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
let balance = component.context.starsContext?.currentState?.balance.value ?? 0
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let balance = presentationStringsFormattedNumber(Int32(component.context.starsContext?.currentState?.balance.value ?? 0), presentationData.dateTimeFormat.groupingSeparator)
//TODO:localize
let attributedText = parseMarkdownIntoAttributedString(
"Your balance is **⭐️\(balance)**",
presentationData.strings.StarsBalance_YourBalance("**⭐️\(balance)**").string,
attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.rootController.navigationBar.primaryTextColor),
@ -100,7 +100,7 @@ public final class StarsBalanceOverlayComponent: Component {
if self.cachedChevronImage == nil || self.cachedChevronImage?.1 !== component.theme {
self.cachedChevronImage = (generateTintedImage(image: UIImage(bundleImageName: "Item List/InlineTextRightArrow"), color: component.theme.rootController.navigationBar.accentTextColor)!, component.theme)
}
let actionText = NSMutableAttributedString(string: "Get More Stars >", font: Font.regular(13.0), textColor: component.theme.rootController.navigationBar.accentTextColor)
let actionText = NSMutableAttributedString(string: presentationData.strings.StarsBalance_GetMoreStars, font: Font.regular(13.0), textColor: component.theme.rootController.navigationBar.accentTextColor)
if let range = actionText.string.range(of: ">"), let chevronImage = self.cachedChevronImage?.0 {
actionText.addAttribute(.attachment, value: chevronImage, range: NSRange(range, in: actionText.string))
actionText.addAttribute(.baselineOffset, value: 1.0, range: NSRange(range, in: actionText.string))
@ -130,7 +130,7 @@ public final class StarsBalanceOverlayComponent: Component {
if actionView.superview == nil {
self.addSubview(actionView)
}
actionView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - actionSize.width) / 2.0), y: 28.0), size: actionSize)
actionView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - actionSize.width) / 2.0), y: 29.0), size: actionSize)
}
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate)

View file

@ -22,8 +22,11 @@ final class StarsBalanceComponent: Component {
let actionAvailable: Bool
let actionIsEnabled: Bool
let actionCooldownUntilTimestamp: Int32?
let actionIcon: UIImage?
let action: () -> Void
let buyAds: (() -> Void)?
let secondaryActionTitle: String?
let secondaryActionIcon: UIImage?
let secondaryAction: (() -> Void)?
let additionalAction: AnyComponent<Empty>?
init(
@ -36,8 +39,11 @@ final class StarsBalanceComponent: Component {
actionAvailable: Bool,
actionIsEnabled: Bool,
actionCooldownUntilTimestamp: Int32? = nil,
actionIcon: UIImage? = nil,
action: @escaping () -> Void,
buyAds: (() -> Void)?,
secondaryActionTitle: String? = nil,
secondaryActionIcon: UIImage? = nil,
secondaryAction: (() -> Void)? = nil,
additionalAction: AnyComponent<Empty>? = nil
) {
self.theme = theme
@ -49,8 +55,11 @@ final class StarsBalanceComponent: Component {
self.actionAvailable = actionAvailable
self.actionIsEnabled = actionIsEnabled
self.actionCooldownUntilTimestamp = actionCooldownUntilTimestamp
self.actionIcon = actionIcon
self.action = action
self.buyAds = buyAds
self.secondaryActionTitle = secondaryActionTitle
self.secondaryActionIcon = secondaryActionIcon
self.secondaryAction = secondaryAction
self.additionalAction = additionalAction
}
@ -76,6 +85,9 @@ final class StarsBalanceComponent: Component {
if lhs.actionCooldownUntilTimestamp != rhs.actionCooldownUntilTimestamp {
return false
}
if lhs.secondaryActionTitle != rhs.secondaryActionTitle {
return false
}
if lhs.count != rhs.count {
return false
}
@ -90,7 +102,7 @@ final class StarsBalanceComponent: Component {
private let title = ComponentView<Empty>()
private let subtitle = ComponentView<Empty>()
private var button = ComponentView<Empty>()
private var buyAdsButton = ComponentView<Empty>()
private var secondaryButton = ComponentView<Empty>()
private var additionalButton = ComponentView<Empty>()
@ -205,18 +217,16 @@ final class StarsBalanceComponent: Component {
if component.actionAvailable {
contentHeight += 12.0
var actionTitle = component.actionTitle
var withdrawWidth = availableSize.width - sideInset * 2.0
if let _ = component.buyAds {
if let _ = component.secondaryAction {
withdrawWidth = (withdrawWidth - 10.0) / 2.0
actionTitle = component.strings.Stars_BotRevenue_Withdraw_WithdrawShort
}
let content: AnyComponentWithIdentity<Empty>
if remainingCooldownSeconds > 0 {
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
VStack([
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 1, component: AnyComponent(BundleIconComponent(name: "Chat List/StatusLockIcon", tintColor: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7)))),
AnyComponentWithIdentity(id: 0, component: AnyComponent(Text(text: stringForRemainingTime(remainingCooldownSeconds), font: Font.with(size: 11.0, weight: .medium, traits: [.monospacedNumbers]), color: component.theme.list.itemCheckColors.fillColor.mixedWith(component.theme.list.itemCheckColors.foregroundColor, alpha: 0.7))))
@ -224,7 +234,17 @@ final class StarsBalanceComponent: Component {
], spacing: 1.0)
))
} else {
content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor)))
var items: [AnyComponentWithIdentity<Empty>] = []
if let icon = component.actionIcon {
items.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(Image(image: icon, tintColor: component.theme.list.itemCheckColors.foregroundColor, size: icon.size))))
}
items.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))))
content = AnyComponentWithIdentity(
id: AnyHashable(0 as Int),
component: AnyComponent(
HStack(items, spacing: 7.0)
)
)
}
let buttonSize = self.button.update(
@ -257,8 +277,14 @@ final class StarsBalanceComponent: Component {
buttonView.frame = buttonFrame
}
if let _ = component.buyAds {
let buttonSize = self.buyAdsButton.update(
if let secondaryActionTitle = component.secondaryActionTitle {
var items: [AnyComponentWithIdentity<Empty>] = []
if let icon = component.secondaryActionIcon {
items.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(Image(image: icon, tintColor: component.theme.list.itemCheckColors.foregroundColor, size: icon.size))))
}
items.append(AnyComponentWithIdentity(id: "label", component: AnyComponent(Text(text: secondaryActionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))))
let buttonSize = self.secondaryButton.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
@ -266,7 +292,12 @@ final class StarsBalanceComponent: Component {
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.8)
),
content: AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: component.strings.Stars_BotRevenue_Withdraw_BuyAds, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor))),
content: AnyComponentWithIdentity(
id: AnyHashable(0 as Int),
component: AnyComponent(
HStack(items, spacing: 7.0)
)
),
isEnabled: component.actionIsEnabled,
allowActionWhenDisabled: false,
displaysProgress: false,
@ -274,13 +305,13 @@ final class StarsBalanceComponent: Component {
guard let self, let component = self.component else {
return
}
component.buyAds?()
component.secondaryAction?()
}
)),
environment: {},
containerSize: CGSize(width: withdrawWidth, height: 50.0)
)
if let buttonView = self.buyAdsButton.view {
if let buttonView = self.secondaryButton.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}

View file

@ -604,7 +604,7 @@ final class StarsStatisticsScreenComponent: Component {
dateTimeFormat: environment.dateTimeFormat,
count: self.starsState?.balances.availableBalance ?? StarsAmount.zero,
rate: self.starsState?.usdRate ?? 0,
actionTitle: strings.Stars_BotRevenue_Withdraw_Withdraw,
actionTitle: strings.Stars_BotRevenue_Withdraw_WithdrawShort,
actionAvailable: true,
actionIsEnabled: self.starsState?.balances.withdrawEnabled ?? true,
actionCooldownUntilTimestamp: self.starsState?.balances.nextWithdrawalTimestamp,
@ -626,7 +626,8 @@ final class StarsStatisticsScreenComponent: Component {
component.withdraw()
}
},
buyAds: { [weak self] in
secondaryActionTitle: strings.Stars_BotRevenue_Withdraw_BuyAds,
secondaryAction: { [weak self] in
guard let self, let component = self.component else {
return
}

View file

@ -23,6 +23,7 @@ import StarsAvatarComponent
import TelegramStringFormatting
import ListItemComponentAdaptor
import ItemListUI
import StarsWithdrawalScreen
private let initialSubscriptionsDisplayedLimit: Int32 = 3
@ -35,6 +36,7 @@ final class StarsTransactionsScreenComponent: Component {
let openTransaction: (StarsContext.State.Transaction) -> Void
let openSubscription: (StarsContext.State.Subscription) -> Void
let buy: () -> Void
let withdraw: () -> Void
let gift: () -> Void
init(
@ -44,6 +46,7 @@ final class StarsTransactionsScreenComponent: Component {
openTransaction: @escaping (StarsContext.State.Transaction) -> Void,
openSubscription: @escaping (StarsContext.State.Subscription) -> Void,
buy: @escaping () -> Void,
withdraw: @escaping () -> Void,
gift: @escaping () -> Void
) {
self.context = context
@ -52,6 +55,7 @@ final class StarsTransactionsScreenComponent: Component {
self.openTransaction = openTransaction
self.openSubscription = openSubscription
self.buy = buy
self.withdraw = withdraw
self.gift = gift
}
@ -602,7 +606,15 @@ final class StarsTransactionsScreenComponent: Component {
contentHeight += descriptionSize.height
contentHeight += 29.0
let withdrawAvailable: Bool
#if DEBUG
withdrawAvailable = "".isEmpty
#else
withdrawAvailable = "".isEmpty
#endif
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
let balanceSize = self.balanceView.update(
transition: .immediate,
@ -617,16 +629,24 @@ final class StarsTransactionsScreenComponent: Component {
dateTimeFormat: environment.dateTimeFormat,
count: self.starsState?.balance ?? StarsAmount.zero,
rate: nil,
actionTitle: environment.strings.Stars_Intro_Buy,
actionTitle: withdrawAvailable ? environment.strings.Stars_Intro_BuyShort : environment.strings.Stars_Intro_Buy,
actionAvailable: !premiumConfiguration.areStarsDisabled && !premiumConfiguration.isPremiumDisabled,
actionIsEnabled: true,
actionIcon: PresentationResourcesItemList.itemListRoundTopupIcon(environment.theme),
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.buy()
},
buyAds: nil,
secondaryActionTitle: withdrawAvailable ? environment.strings.Stars_Intro_Withdraw : nil,
secondaryActionIcon: withdrawAvailable ? PresentationResourcesItemList.itemListRoundWithdrawIcon(environment.theme) : nil,
secondaryAction: withdrawAvailable ? { [weak self] in
guard let self, let component = self.component else {
return
}
component.withdraw()
} : nil,
additionalAction: (premiumConfiguration.starsGiftsPurchaseAvailable && !premiumConfiguration.isPremiumDisabled) ? AnyComponent(
Button(
content: AnyComponent(
@ -1057,6 +1077,7 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
self.subscriptionsContext = context.engine.payments.peerStarsSubscriptionsContext(starsContext: starsContext)
var buyImpl: (() -> Void)?
var withdrawImpl: (() -> Void)?
var giftImpl: (() -> Void)?
var openTransactionImpl: ((StarsContext.State.Transaction) -> Void)?
var openSubscriptionImpl: ((StarsContext.State.Subscription) -> Void)?
@ -1073,6 +1094,9 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
buy: {
buyImpl?()
},
withdraw: {
withdrawImpl?()
},
gift: {
giftImpl?()
}
@ -1168,6 +1192,47 @@ public final class StarsTransactionsScreen: ViewControllerComponentContainer {
})
}
withdrawImpl = { [weak self] in
guard let self else {
return
}
let _ = (context.engine.peers.checkStarsRevenueWithdrawalAvailability()
|> deliverOnMainQueue).start(error: { [weak self] error in
guard let self else {
return
}
switch error {
case .serverProvided:
return
case .requestPassword:
let controller = self.context.sharedContext.makeStarsWithdrawalScreen(context: context, completion: { [weak self] amount in
guard let self else {
return
}
let controller = confirmStarsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: amount, present: { [weak self] c, a in
self?.present(c, in: .window(.root))
}, completion: { url in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
Queue.mainQueue().after(2.0) {
context.starsContext?.load(force: true)
}
})
self.present(controller, in: .window(.root))
})
self.push(controller)
default:
let controller = starsRevenueWithdrawalController(context: context, peerId: context.account.peerId, amount: 0, initialError: error, present: { [weak self] c, a in
self?.present(c, in: .window(.root))
}, completion: { _ in
})
self.present(controller, in: .window(.root))
}
})
}
giftImpl = { [weak self] in
guard let self else {
return

View file

@ -116,6 +116,14 @@ private final class SheetContent: CombinedComponent {
minAmount = configuration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = status.balances.availableBalance
amountLabel = nil
case .accountWithdraw:
titleString = environment.strings.Stars_Withdraw_Title
amountTitle = environment.strings.Stars_Withdraw_AmountTitle
amountPlaceholder = environment.strings.Stars_Withdraw_AmountPlaceholder
minAmount = configuration.minWithdrawAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
maxAmount = state.balance
amountLabel = nil
case .paidMedia:
titleString = environment.strings.Stars_PaidContent_Title
amountTitle = environment.strings.Stars_PaidContent_AmountTitle
@ -124,7 +132,6 @@ private final class SheetContent: CombinedComponent {
minAmount = StarsAmount(value: 1, nanos: 0)
maxAmount = configuration.maxPaidMediaAmount.flatMap { StarsAmount(value: $0, nanos: 0) }
if let usdWithdrawRate = configuration.usdWithdrawRate, let amount = state.amount, amount > StarsAmount.zero {
let usdRate = Double(usdWithdrawRate) / 1000.0 / 100.0
amountLabel = "\(formatTonUsdValue(amount.value, divide: false, rate: usdRate, dateTimeFormat: environment.dateTimeFormat))"
@ -153,7 +160,9 @@ private final class SheetContent: CombinedComponent {
contentSize.height += 40.0
let balance: StarsAmount?
if case .reaction = component.mode {
if case .accountWithdraw = component.mode {
balance = state.balance
} else if case .reaction = component.mode {
balance = state.balance
} else if case let .withdraw(starsState) = component.mode {
balance = starsState.balances.availableBalance
@ -378,7 +387,9 @@ private final class SheetContent: CombinedComponent {
var amount: StarsAmount?
switch mode {
case let .withdraw(stats):
amount = stats.balances.availableBalance
amount = StarsAmount(value: stats.balances.availableBalance.value, nanos: 0)
case .accountWithdraw:
amount = context.starsContext?.currentState.flatMap { StarsAmount(value: $0.balance.value, nanos: 0) }
case let .paidMedia(initialValue):
amount = initialValue.flatMap { StarsAmount(value: $0, nanos: 0) }
case .reaction:
@ -500,6 +511,7 @@ private final class StarsWithdrawSheetComponent: CombinedComponent {
public final class StarsWithdrawScreen: ViewControllerComponentContainer {
public enum Mode: Equatable {
case withdraw(StarsRevenueStats)
case accountWithdraw
case paidMedia(Int64?)
case reaction(Int64?)
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "hide_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "pin_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "pin_48.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "show_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "transfer_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "wear_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -2591,7 +2591,7 @@ extension ChatControllerImpl {
}
})
}, openPeerInfo: { [weak self] in
self?.navigationButtonAction(.openChatInfo(expandAvatar: false, recommendedChannels: false))
self?.navigationButtonAction(.openChatInfo(expandAvatar: false, section: nil))
}, togglePeerNotifications: { [weak self] in
if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId {
let _ = strongSelf.context.engine.peers.togglePeerMuted(peerId: peerId, threadId: strongSelf.chatLocation.threadId).startStandalone()

View file

@ -379,7 +379,7 @@ extension ChatControllerImpl {
strongSelf.present(actionSheet, in: .window(.root))
})
}
case let .openChatInfo(expandAvatar, recommendedChannels):
case let .openChatInfo(expandAvatar, section):
let _ = self.presentVoiceMessageDiscardAlert(action: {
switch self.chatLocationInfoData {
case let .peer(peerView):
@ -400,7 +400,16 @@ extension ChatControllerImpl {
if let validLayout = strongSelf.validLayout, validLayout.deviceMetrics.type == .tablet {
expandAvatar = false
}
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: recommendedChannels ? .recommendedChannels : .generic, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: strongSelf.inviteRequestsContext) {
let mode: PeerInfoControllerMode
switch section {
case .groupsInCommon:
mode = .groupsInCommon
case .recommendedChannels:
mode = .recommendedChannels
default:
mode = .generic
}
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: expandAvatar, fromChat: true, requestsContext: strongSelf.inviteRequestsContext) {
strongSelf.effectiveNavigationController?.pushViewController(infoController)
}
}

View file

@ -130,17 +130,19 @@ extension ChatControllerImpl {
if case let .peer(currentPeerId) = self.chatLocation, peer?.id == currentPeerId {
switch navigation {
case let .info(params):
var recommendedChannels = false
var section: ChatNavigationButtonAction.ChatInfoSection?
if let params {
if params.switchToRecommendedChannels {
recommendedChannels = true
section = .recommendedChannels
} else if params.switchToGroupsInCommon {
section = .groupsInCommon
}
if params.ignoreInSavedMessages && currentPeerId == self.context.account.peerId {
self.playShakeAnimation()
return
}
}
self.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar, recommendedChannels: recommendedChannels))
self.navigationButtonAction(.openChatInfo(expandAvatar: expandAvatar, section: section))
case let .chat(textInputState, _, _):
if let textInputState = textInputState {
self.updateChatPresentationInterfaceState(animated: true, interactive: true, {
@ -189,8 +191,12 @@ extension ChatControllerImpl {
if let fromReactionMessageId = fromReactionMessageId {
mode = .reaction(fromReactionMessageId)
}
if case let .info(params) = navigation, let params, params.switchToRecommendedChannels {
mode = .recommendedChannels
if case let .info(params) = navigation, let params {
if params.switchToRecommendedChannels {
mode = .recommendedChannels
} else if params.switchToGroupsInCommon {
mode = .groupsInCommon
}
}
if peer.id == strongSelf.context.account.peerId {
mode = .myProfile

View file

@ -21,12 +21,14 @@ extension ChatControllerImpl {
return
}
if let sendPaidMessageStars = self.presentationInterfaceState.sendPaidMessageStars {
let totalAmount = sendPaidMessageStars.value * Int64(count)
let _ = (ApplicationSpecificNotice.dismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id)
|> deliverOnMainQueue).start(next: { [weak self] dismissedAmount in
guard let self, let starsContext = self.context.starsContext else {
return
}
if let dismissedAmount, dismissedAmount == sendPaidMessageStars.value, let currentState = starsContext.currentState, currentState.balance > sendPaidMessageStars {
if let dismissedAmount, dismissedAmount == sendPaidMessageStars.value, let currentState = starsContext.currentState, currentState.balance.value > totalAmount, count < 3 && totalAmount < 100 {
completion(true)
self.displayPaidMessageUndo(count: count, amount: sendPaidMessageStars)
} else {
@ -52,14 +54,14 @@ extension ChatControllerImpl {
let _ = ApplicationSpecificNotice.setDismissedPaidMessageWarningNamespace(accountManager: self.context.sharedContext.accountManager, peerId: peer.id, amount: sendPaidMessageStars.value).start()
}
if let currentState = starsContext.currentState, currentState.balance < sendPaidMessageStars {
if let currentState = starsContext.currentState, currentState.balance.value < totalAmount {
let _ = (self.context.engine.payments.starsTopUpOptions()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] options in
guard let self else {
return
}
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: sendPaidMessageStars.value), completion: { _ in
let controller = self.context.sharedContext.makeStarsPurchaseScreen(context: self.context, starsContext: starsContext, options: options, purpose: .sendMessage(peerId: peer.id, requiredStars: totalAmount), completion: { _ in
completion(false)
})
self.push(controller)
@ -89,18 +91,11 @@ extension ChatControllerImpl {
self.context.engine.messages.forceSendPostponedPaidMessage(peerId: peerId)
}
//TODO:localize
let title: String
if count > 1 {
title = "\(count) Messages Sent"
} else {
title = "Message Sent"
}
let title = self.presentationData.strings.Chat_PaidMessage_Sent_Title(count)
let text = self.presentationData.strings.Chat_PaidMessage_Sent_Text(self.presentationData.strings.Chat_PaidMessage_Sent_Text_Stars(Int32(amount.value * Int64(count)))).string
let textItems: [AnimatedTextComponent.Item] = [
AnimatedTextComponent.Item(id: 0, content: .text("You paid \(amount.value * Int64(count)) Stars"))
AnimatedTextComponent.Item(id: 0, content: .text(text))
]
let controller = UndoOverlayController(presentationData: self.presentationData, content: .starsSent(context: self.context, title: title, text: textItems), elevatedLayout: false, position: .top, action: { [weak self] action in
guard let self else {
return false

View file

@ -2271,36 +2271,41 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }.updatedInputMode { current in
if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil {
return .media(mode: mode, expanded: nil, focused: focused)
strongSelf.presentPaidMessageAlertIfNeeded(completion: { [weak self] postpone in
guard let strongSelf = self else {
return
}
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil) }.updatedInputMode { current in
if case let .media(mode, maybeExpanded, focused) = current, maybeExpanded != nil {
return .media(mode: mode, expanded: nil, focused: focused)
}
return current
}
return current
})
}
}, nil)
var messages = [EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
if silentPosting {
messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true)
strongSelf.sendMessages(messages)
} else if schedule {
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in
if let strongSelf = self {
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
strongSelf.sendMessages(transformedMessages)
}
})
} else {
messages = strongSelf.transformEnqueueMessages(messages)
strongSelf.sendMessages(messages)
}
}, nil)
var messages = [EnqueueMessage.message(text: "", attributes: [], inlineStickers: [:], mediaReference: fileReference.abstract, threadId: strongSelf.chatLocation.threadId, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
if silentPosting {
messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true)
strongSelf.sendMessages(messages)
} else if schedule {
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime in
if let strongSelf = self {
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
strongSelf.sendMessages(transformedMessages)
}
})
} else {
messages = strongSelf.transformEnqueueMessages(messages)
strongSelf.sendMessages(messages)
}
})
}
return true
}, sendBotContextResultAsGif: { [weak self] collection, result, sourceView, sourceRect, silentPosting, resetTextInputState in
@ -4826,7 +4831,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Info"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
f(.dismissWithoutContent)
self?.navigationButtonAction(.openChatInfo(expandAvatar: true, recommendedChannels: false))
self?.navigationButtonAction(.openChatInfo(expandAvatar: true, section: nil))
})))
if canViewStats {
@ -5157,7 +5162,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
chatInfoButtonItem.target = self
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo(expandAvatar: true, recommendedChannels: false), buttonItem: chatInfoButtonItem)
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo(expandAvatar: true, section: nil), buttonItem: chatInfoButtonItem)
self.moreBarButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: self.presentationData.theme.rootController.navigationBar.buttonColor)))
self.moreInfoNavigationButton = ChatNavigationButton(action: .toggleInfoPanel, buttonItem: UIBarButtonItem(customDisplayNode: self.moreBarButton)!)
@ -5179,7 +5184,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.navigationItem.titleView = self.chatTitleView
self.chatTitleView?.pressed = { [weak self] in
self?.navigationButtonAction(.openChatInfo(expandAvatar: false, recommendedChannels: false))
self?.navigationButtonAction(.openChatInfo(expandAvatar: false, section: nil))
}
self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
@ -5779,10 +5784,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
contactStatus = ChatContactStatus(canAddContact: false, canReportIrrelevantLocation: canReportIrrelevantLocation, peerStatusSettings: cachedData.peerStatusSettings, invitedBy: invitedBy, managingBot: managingBot)
if let peer = peerView.peers[peerView.peerId] as? TelegramChannel, peer.flags.contains(.isCreator) || peer.adminRights != nil {
} else {
sendPaidMessageStars = cachedData.sendPaidMessageStars
if let channel = peerView.peers[peerView.peerId] as? TelegramChannel {
if channel.flags.contains(.isCreator) || channel.adminRights != nil {
} else {
sendPaidMessageStars = channel.sendPaidMessageStars
}
}
}

View file

@ -89,14 +89,13 @@ final class ChatFeePanelNode: ASDisplayNode {
if self.theme !== interfaceState.theme {
self.theme = interfaceState.theme
self.separatorNode.backgroundColor = interfaceState.theme.rootController.navigationBar.separatorColor
//TODO:localize
self.removeTextNode.attributedText = NSAttributedString(string: "Remove Fee", font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor)
self.removeTextNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_PaidMessageFee_RemoveFee, font: Font.regular(17.0), textColor: interfaceState.theme.chat.inputPanel.panelControlAccentColor)
}
let paidMessageStars = interfaceState.contactStatus?.peerStatusSettings?.paidMessageStars?.value ?? 0
if let peer = interfaceState.renderedPeer?.peer.flatMap(EnginePeer.init) {
let attributedText = NSMutableAttributedString(string: "\(peer.compactDisplayTitle) must pay ⭐️\(paidMessageStars) for each message to you.", font: Font.regular(12.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor)
let attributedText = NSMutableAttributedString(string: interfaceState.strings.Chat_PaidMessageFee_Text(peer.compactDisplayTitle, "⭐️\(paidMessageStars)").string, font: Font.regular(12.0), textColor: interfaceState.theme.rootController.navigationBar.secondaryTextColor)
let range = (attributedText.string as NSString).range(of: "⭐️")
if range.location != NSNotFound {
attributedText.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: 0, file: nil, custom: .stars(tinted: true)), range: range)

View file

@ -11,6 +11,7 @@ import ChatMessageItemCommon
import TextFormat
import Markdown
import Display
import TelegramStringFormatting
struct ChatHistoryEntriesForViewState {
private var messageStableIdToLocalId: [UInt32: Int64] = [:]
@ -67,17 +68,19 @@ func chatHistoryEntriesForView(
var entries: [ChatHistoryEntry] = []
var adminRanks: [PeerId: CachedChannelAdminRank] = [:]
var stickersEnabled = true
var channelPeer: Peer?
var chatPeer: Peer?
if let peerId = location.peerId, peerId.namespace == Namespaces.Peer.CloudChannel {
for additionalEntry in view.additionalData {
if case let .cacheEntry(id, data) = additionalEntry {
if id == cachedChannelAdminRanksEntryId(peerId: peerId), let data = data?.get(CachedChannelAdminRanks.self) {
adminRanks = data.ranks
}
} else if case let .peer(_, peer) = additionalEntry, let channel = peer as? TelegramChannel, !channel.flags.contains(.isGigagroup) {
channelPeer = channel
if let defaultBannedRights = channel.defaultBannedRights, defaultBannedRights.flags.contains(.banSendStickers) {
stickersEnabled = false
} else if case let .peer(_, peer) = additionalEntry {
chatPeer = peer
if let channel = peer as? TelegramChannel, !channel.flags.contains(.isGigagroup) {
if let defaultBannedRights = channel.defaultBannedRights, defaultBannedRights.flags.contains(.banSendStickers) {
stickersEnabled = false
}
}
}
}
@ -87,7 +90,7 @@ func chatHistoryEntriesForView(
if (associatedData.subject?.isService ?? false) {
} else {
if let peer = channelPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) {
if let peer = chatPeer as? TelegramChannel, case .broadcast = peer.info, case .member = peer.participationStatus, !peer.flags.contains(.isCreator) {
joinMessage = Message(
stableId: UInt32.max - 1000,
stableVersion: 0,
@ -103,7 +106,7 @@ func chatHistoryEntriesForView(
localTags: [],
customTags: [],
forwardInfo: nil,
author: channelPeer,
author: chatPeer,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .joinedChannel)],
@ -436,10 +439,92 @@ func chatHistoryEntriesForView(
} else if let cachedPeerData = cachedPeerData as? CachedUserData {
if let botInfo = cachedPeerData.botInfo, !botInfo.description.isEmpty {
entries.insert(.ChatInfoEntry(.botInfo(title: presentationData.strings.Bot_DescriptionTitle, text: botInfo.description, photo: botInfo.photo, video: botInfo.video), presentationData), at: 0)
} else if let peerStatusSettings = cachedPeerData.peerStatusSettings, peerStatusSettings.registrationDate != nil || peerStatusSettings.phoneCountry != nil || peerStatusSettings.locationCountry != nil {
} else if let peerStatusSettings = cachedPeerData.peerStatusSettings, peerStatusSettings.registrationDate != nil || peerStatusSettings.phoneCountry != nil {
if peerStatusSettings.flags.contains(.canAddContact) || peerStatusSettings.flags.contains(.canReport) || peerStatusSettings.flags.contains(.canBlock) {
let title = chatPeer.flatMap(EnginePeer.init)?.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) ?? ""
entries.insert(.ChatInfoEntry(.userInfo(title: title, registrationDate: peerStatusSettings.registrationDate, phoneCountry: peerStatusSettings.phoneCountry, locationCountry: peerStatusSettings.locationCountry, groupsInCommon: []), presentationData), at: 0)
if let chatPeer, let photoChangeDate = peerStatusSettings.photoChangeDate, photoChangeDate > 0 {
let timeText = stringForIntervalSinceUpdateAction(strings: presentationData.strings, value: photoChangeDate)
let text = presentationData.strings.Chat_NonContactUser_UpdatedPhoto(timeText)
var entities: [MessageTextEntity] = []
for range in text.ranges {
entities.append(MessageTextEntity(range: range.range.lowerBound ..< range.range.upperBound, type: .Bold))
}
let message = Message(
stableId: UInt32.max - 1001,
stableVersion: 0,
id: MessageId(peerId: chatPeer.id, namespace: Namespaces.Message.Local, id: -1),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: 2,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: chatPeer,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .customText(
text: text.string,
entities: entities,
additionalAttributes: nil
))],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: 0)
}
if let chatPeer, let nameChangeDate = peerStatusSettings.nameChangeDate, nameChangeDate > 0 {
let timeText = stringForIntervalSinceUpdateAction(strings: presentationData.strings, value: nameChangeDate)
let text = presentationData.strings.Chat_NonContactUser_UpdatedName(timeText)
var entities: [MessageTextEntity] = []
for range in text.ranges {
entities.append(MessageTextEntity(range: range.range.lowerBound ..< range.range.upperBound, type: .Bold))
}
let message = Message(
stableId: UInt32.max - 1002,
stableVersion: 0,
id: MessageId(peerId: chatPeer.id, namespace: Namespaces.Message.Local, id: -2),
globallyUniqueId: nil,
groupingKey: nil,
groupInfo: nil,
threadId: nil,
timestamp: 1,
flags: [.Incoming],
tags: [],
globalTags: [],
localTags: [],
customTags: [],
forwardInfo: nil,
author: chatPeer,
text: "",
attributes: [],
media: [TelegramMediaAction(action: .customText(
text: text.string,
entities: entities,
additionalAttributes: nil
))],
peers: SimpleDictionary<PeerId, Peer>(),
associatedMessages: SimpleDictionary<MessageId, Message>(),
associatedMessageIds: [],
associatedMedia: [:],
associatedThreadInfo: nil,
associatedStories: [:]
)
entries.insert(.MessageEntry(message, presentationData, false, nil, .none, ChatMessageEntryAttributes(rank: nil, isContact: false, contentTypeHint: .generic, updatingMedia: nil, isPlaying: false, isCentered: false, authorStoryStats: nil)), at: 0)
}
if let peer = chatPeer.flatMap(EnginePeer.init) {
entries.insert(.ChatInfoEntry(.userInfo(peer: peer, verification: cachedPeerData.verification, registrationDate: peerStatusSettings.registrationDate, phoneCountry: peerStatusSettings.phoneCountry, groupsInCommonCount: cachedPeerData.commonGroupCount), presentationData), at: 0)
}
}
}
} else {

View file

@ -227,7 +227,7 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
let item: ListViewItem
switch mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders || message.timestamp < 10)
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool
switch displayHeaders {
@ -260,8 +260,8 @@ private func mappedInsertEntries(context: AccountContext, chatLocation: ChatLoca
switch data {
case let .botInfo(title, text, photo, video):
item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
case let .userInfo(title, registrationDate, phoneCountry, locationCountry, groupsInCommon):
item = ChatUserInfoItem(title: title, registrationDate: registrationDate, phoneCountry: phoneCountry, locationCountry: locationCountry, groupsInCommon: groupsInCommon, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
case let .userInfo(peer, verification, registrationDate, phoneCountry, groupsInCommonCount):
item = ChatUserInfoItem(peer: peer, verification: verification, registrationDate: registrationDate, phoneCountry: phoneCountry, groupsInCommonCount: groupsInCommonCount, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
}
return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .SearchEntry(theme, strings):
@ -284,7 +284,7 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca
let item: ListViewItem
switch mode {
case .bubbles:
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders)
item = ChatMessageItemImpl(presentationData: presentationData, context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, content: .message(message: message, read: read, selection: selection, attributes: attributes, location: location), disableDate: disableFloatingDateHeaders || message.timestamp < 10)
case let .list(_, _, _, displayHeaders, hintLinks, isGlobalSearch):
let displayHeader: Bool
switch displayHeaders {
@ -317,8 +317,8 @@ private func mappedUpdateEntries(context: AccountContext, chatLocation: ChatLoca
switch data {
case let .botInfo(title, text, photo, video):
item = ChatBotInfoItem(title: title, text: text, photo: photo, video: video, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
case let .userInfo(title, registrationDate, phoneCountry, locationCountry, groupsInCommon):
item = ChatUserInfoItem(title: title, registrationDate: registrationDate, phoneCountry: phoneCountry, locationCountry: locationCountry, groupsInCommon: groupsInCommon, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
case let .userInfo(peer, verification, registrationDate, phoneCountry, groupsInCommonCount):
item = ChatUserInfoItem(peer: peer, verification: verification, registrationDate: registrationDate, phoneCountry: phoneCountry, groupsInCommonCount: groupsInCommonCount, controllerInteraction: controllerInteraction, presentationData: presentationData, context: context)
}
return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint)
case let .SearchEntry(theme, strings):

View file

@ -426,7 +426,7 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode {
}
if let view = self.scrubber.view {
view.frame = CGRect(origin: CGPoint(x: max(leftInset + 45.0, floorToScreenPixels((width - view.bounds.width) / 2.0)), y: 7.0 - UIScreenPixel), size: view.bounds.size)
view.frame = CGRect(origin: CGPoint(x: min(width - innerSize.width - view.bounds.width, max(leftInset + 45.0, floorToScreenPixels((width - view.bounds.width) / 2.0))), y: 7.0 - UIScreenPixel), size: view.bounds.size)
}
let panelHeight = defaultHeight(metrics: metrics)

View file

@ -1895,7 +1895,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
peerUpdated = true
}
if peerUpdated || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id {
if peerUpdated || previousState?.interfaceState.silentPosting != interfaceState.interfaceState.silentPosting || themeUpdated || !self.initializedPlaceholder || previousState?.keyboardButtonsMessage?.id != interfaceState.keyboardButtonsMessage?.id || previousState?.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder != interfaceState.keyboardButtonsMessage?.visibleReplyMarkupPlaceholder || dismissedButtonMessageUpdated || replyMessageUpdated || (previousState?.interfaceState.editMessage == nil) != (interfaceState.interfaceState.editMessage == nil) || previousState?.forumTopicData != interfaceState.forumTopicData || previousState?.replyMessage?.id != interfaceState.replyMessage?.id || previousState?.sendPaidMessageStars != interfaceState.sendPaidMessageStars {
self.initializedPlaceholder = true
var placeholder: String = ""

View file

@ -3314,6 +3314,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return StarsWithdrawScreen(context: context, mode: .withdraw(stats), completion: completion)
}
public func makeStarsWithdrawalScreen(context: AccountContext, completion: @escaping (Int64) -> Void) -> ViewController {
return StarsWithdrawScreen(context: context, mode: .accountWithdraw, completion: completion)
}
public func makeStarsGiftScreen(context: AccountContext, message: EngineMessage) -> ViewController {
return StarsTransactionScreen(context: context, subject: .gift(message))
}
@ -3474,6 +3478,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
var forumTopicThread: ChatReplyThreadMessage?
var switchToRecommendedChannels = false
var switchToGifts = false
var switchToGroupsInCommon = false
switch mode {
case let .forumTopic(thread):
forumTopicThread = thread
@ -3481,10 +3486,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
switchToRecommendedChannels = true
case .gifts:
switchToGifts = true
case .groupsInCommon:
switchToGroupsInCommon = true
default:
break
}
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], forumTopicThread: forumTopicThread, switchToRecommendedChannels: switchToRecommendedChannels, switchToGifts: switchToGifts)
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [], forumTopicThread: forumTopicThread, switchToRecommendedChannels: switchToRecommendedChannels, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon)
} else if peer is TelegramUser {
var nearbyPeerDistance: Int32?
var reactionSourceMessageId: MessageId?
@ -3493,6 +3500,7 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
var forumTopicThread: ChatReplyThreadMessage?
var isMyProfile = false
var switchToGifts = false
var switchToGroupsInCommon = false
switch mode {
case let .nearbyPeer(distance):
@ -3514,10 +3522,12 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
case .myProfileGifts:
isMyProfile = true
switchToGifts = true
case .groupsInCommon:
switchToGroupsInCommon = true
default:
break
}
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, switchToGifts: switchToGifts)
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nearbyPeerDistance, reactionSourceMessageId: reactionSourceMessageId, callMessages: callMessages, isMyProfile: isMyProfile, hintGroupInCommon: hintGroupInCommon, forumTopicThread: forumTopicThread, switchToGifts: switchToGifts, switchToGroupsInCommon: switchToGroupsInCommon)
} else if peer is TelegramSecretChat {
return PeerInfoScreenImpl(context: context, updatedPresentationData: updatedPresentationData, peerId: peer.id, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: isOpenedFromChat, nearbyPeerDistance: nil, reactionSourceMessageId: nil, callMessages: [])
}