diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 7e7d949861..8a67272ca8 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15676,9 +15676,9 @@ Error: %8$@"; "Gift.Craft.Title" = "Craft Gift"; "Gift.Craft.Description" = "Add up to **4 gifts** to craft new\n**$ %@**.\n\nIf crafting fails, all used gifts\nwill be lost."; -"Gift.Craft.ViewVariants" = "View all new variants"; -"Gift.Craft.BackdropTooltip" = "There's **%1$@%** chance the crafted gift will have **%2$@** backdrop."; -"Gift.Craft.SymbolTooltip" = "There's **%1$@%** chance the crafted gift will have **%2$@** symbol."; +"Gift.Craft.ViewVariants" = "View all craftable models"; +"Gift.Craft.BackdropTooltip" = "**%1$@%** chance the crafted gift will have **%2$@** backdrop."; +"Gift.Craft.SymbolTooltip" = "**%1$@%** chance the crafted gift will have **%2$@** symbol."; "Gift.Craft.SuccessChanceSuffix" = "% Success Chance"; "Gift.Craft.Craft" = "Craft %@"; "Gift.Craft.Crafting.Title" = "Crafting"; @@ -15722,5 +15722,35 @@ Error: %8$@"; "Gift.View.OnSale" = "On sale for %@"; +"Gift.Store.Balance.MyStars" = "My Stars"; +"Gift.Store.Balance.MyTon" = "My TON"; + +"Resolve.GiftErrorNotFound" = "Sorry, this gift doesn't seem to exist."; +"Resolve.GiftErrorBurned" = "Sorry, this gift has already been burned."; + +"Notification.StarGift.Burned" = "burned"; + +"AuthConfirmation.Title" = "Log in to **%@**"; +"AuthConfirmation.Description" = "This site will receive your **name**,\n**username** and **profile photo**."; +"AuthConfirmation.Device" = "Device"; +"AuthConfirmation.IpAddress" ="IP Address"; +"AuthConfirmation.Info" ="This login attempt came from the device above."; +"AuthConfirmation.AllowMessages" = "Allow Messages"; +"AuthConfirmation.AllowMessagesInfo" = "This will allow %@ to message you."; +"AuthConfirmation.Cancel" = "Cancel"; +"AuthConfirmation.LogIn" = "Log In"; + +"AuthConfirmation.PhoneNumberConfirmation.Title" = "Phone Number"; +"AuthConfirmation.PhoneNumberConfirmation.Text" = "**%1$@** wants to access your phone number **%2$@**.\n\nAllow access?"; +"AuthConfirmation.PhoneNumberConfirmation.Deny" = "Deny"; +"AuthConfirmation.PhoneNumberConfirmation.Allow" = "Allow"; + +"AuthConfirmation.LoginSuccess.Title" = "Login Successful"; +"AuthConfirmation.LoginSuccess.Text" = "You are now logged in to [%@]()."; +"AuthConfirmation.LoginSuccess.TextNoNumber" = "You are logged in to [%@](), but you didn't grant access to your phone number."; + +"AuthConfirmation.LoginFail.Title" = "Login Failed"; +"AuthConfirmation.LoginFail.Text" = "Please try logging in to [%@]() again."; + "Notification.GroupCreatorChangePending" = "%2$@ will become the new main admin in 7 days if %1$@ does not return."; "Notification.GroupCreatorChangeApplied" = "%1$@ has transferred ownership of the group to %2$@."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 5f3b276946..80a15073c4 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -316,7 +316,6 @@ public enum ResolvedUrl { case boost(peerId: PeerId?, status: ChannelBoostStatus?, myBoostStatus: MyBoostStatus?) case premiumGiftCode(slug: String) case premiumMultiGift(reference: String?) - case collectible(gift: StarGift.UniqueGift?) case auction(auction: GiftAuctionContext?) case messageLink(link: TelegramResolvedMessageLink?) case stars @@ -328,6 +327,13 @@ public enum ResolvedUrl { case unknownDeepLink(path: String) case oauth(url: String) + public enum ResolvedCollectible { + case gift(StarGift.UniqueGift) + case invalidSlug + case alreadyBurned + } + case collectible(ResolvedCollectible) + public enum ChatsSection { case search case edit diff --git a/submodules/SettingsUI/Sources/ArchiveSettingsController.swift b/submodules/SettingsUI/Sources/ArchiveSettingsController.swift index 33f75c86cc..f92f453a1e 100644 --- a/submodules/SettingsUI/Sources/ArchiveSettingsController.swift +++ b/submodules/SettingsUI/Sources/ArchiveSettingsController.swift @@ -89,7 +89,7 @@ private enum ArchiveSettingsControllerEntry: ItemListNodeEntry { case .unmutedHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_UnmutedChatsHeader, sectionId: self.section) case let .unmutedValue(value): - return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_KeepArchived, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.ArchiveSettings_KeepArchived, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateUnmuted(value) }) case .unmutedFooter: @@ -97,7 +97,7 @@ private enum ArchiveSettingsControllerEntry: ItemListNodeEntry { case .foldersHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_FolderChatsHeader, sectionId: self.section) case let .foldersValue(value): - return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_KeepArchived, value: value, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.ArchiveSettings_KeepArchived, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateFolders(value) }) case .foldersFooter: @@ -105,7 +105,7 @@ private enum ArchiveSettingsControllerEntry: ItemListNodeEntry { case .unknownHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_UnknownChatsHeader, sectionId: self.section) case let .unknownValue(isOn, isLocked): - return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_AutomaticallyArchive, value: isOn, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: presentationData.strings.ArchiveSettings_AutomaticallyArchive, value: isOn, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { value in arguments.updateUnknown(value) }, activatedWhileDisabled: { arguments.updateUnknown(nil) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 4325a21930..1da0fb9199 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -302,7 +302,6 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case starGiftPurchaseOffer(gift: StarGift, amount: CurrencyAmount, expireDate: Int32, isAccepted: Bool, isDeclined: Bool) case starGiftPurchaseOfferDeclined(gift: StarGift, amount: CurrencyAmount, hasExpired: Bool) case groupCreatorChange(GroupCreatorChange) - case starGiftCraftFail public init(decoder: PostboxDecoder) { let rawValue: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) @@ -474,8 +473,6 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { self = .starGiftPurchaseOffer(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, amount: decoder.decodeCodable(CurrencyAmount.self, forKey: "amount") ?? CurrencyAmount(amount: .zero, currency: .stars), expireDate: decoder.decodeInt32ForKey("expireDate", orElse: 0), isAccepted: decoder.decodeBoolForKey("isAccepted", orElse: false), isDeclined: decoder.decodeBoolForKey("isDeclined", orElse: false)) case 57: self = .starGiftPurchaseOfferDeclined(gift: decoder.decodeObjectForKey("gift", decoder: { StarGift(decoder: $0) }) as! StarGift, amount: decoder.decodeCodable(CurrencyAmount.self, forKey: "amount")!, hasExpired: decoder.decodeBoolForKey("hasExpired", orElse: false)) - case 58: - self = .starGiftCraftFail case 59: self = .groupCreatorChange(decoder.decodeCodable(GroupCreatorChange.self, forKey: "d") ?? GroupCreatorChange(kind: .pending, targetPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)))) default: @@ -969,8 +966,6 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { encoder.encodeObject(gift, forKey: "gift") encoder.encodeCodable(amount, forKey: "amount") encoder.encodeBool(hasExpired, forKey: "hasExpired") - case .starGiftCraftFail: - encoder.encodeInt32(58, forKey: "_rawValue") case let .groupCreatorChange(groupCreatorChange): encoder.encodeInt32(59, forKey: "_rawValue") encoder.encodeCodable(groupCreatorChange, forKey: "d") diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift index 83c8daf9fe..2d35a58765 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift @@ -199,6 +199,11 @@ public enum MessageActionUrlAuthResult { case request(domain: String, bot: Peer, clientData: ClientData?, flags: Flags) } +public enum MessageActionUrlAuthError { + case generic + case urlExpired +} + public enum MessageActionUrlSubject { case message(id: MessageId, buttonId: Int32) case url(String) @@ -259,7 +264,7 @@ func _internal_requestMessageActionUrlAuth(account: Account, subject: MessageAct } } -func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal { +func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal { var flags: Int32 = 0 if allowWriteAccess { flags |= Int32(1 << 0) @@ -292,13 +297,10 @@ func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActi return request - |> `catch` { _ -> Signal in - return .single(nil) + |> mapError { _ -> MessageActionUrlAuthError in + return .generic } |> map { result -> MessageActionUrlAuthResult in - guard let result = result else { - return .default - } switch result { case let .urlAuthResultAccepted(urlAuthResultAcceptedData): let url = urlAuthResultAcceptedData.url diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 9c6a9a4f49..0b25931122 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -99,7 +99,7 @@ public extension TelegramEngine { return _internal_requestMessageActionUrlAuth(account: self.account, subject: subject) } - public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal { + public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal { return _internal_acceptMessageActionUrlAuth(account: self.account, subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift index 9dac20fba5..f54b2155ba 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/StarGifts.swift @@ -3592,33 +3592,36 @@ extension StarGift.UniqueGift.Attribute { } } - public enum GetUniqueStarGiftError { case generic - case burned + case invalidSlug + case alreadyBurned } -func _internal_getUniqueStarGift(account: Account, slug: String) -> Signal { +func _internal_getUniqueStarGift(account: Account, slug: String) -> Signal { return account.network.request(Api.functions.payments.getUniqueStarGift(slug: slug)) |> mapError { error -> GetUniqueStarGiftError in if error.errorDescription == "STARGIFT_ALREADY_BURNED" { - return .burned + return .alreadyBurned + } else if error.errorDescription == "STARGIFT_SLUG_INVALID" { + return .invalidSlug } return .generic } - |> mapToSignal { result -> Signal in + |> mapToSignal { result -> Signal in switch result { case let .uniqueStarGift(uniqueStarGiftData): let (gift, chats, users) = (uniqueStarGiftData.gift, uniqueStarGiftData.chats, uniqueStarGiftData.users) - return account.postbox.transaction { transaction in + return account.postbox.transaction { transaction -> Signal in let parsedPeers = AccumulatedPeers(chats: chats, users: users) updatePeers(transaction: transaction, accountPeerId: account.peerId, peers: parsedPeers) guard case let .unique(uniqueGift) = StarGift(apiStarGift: gift) else { - return nil + return .fail(.invalidSlug) } - return uniqueGift + return .single(uniqueGift) } |> castError(GetUniqueStarGiftError.self) + |> switchToLatest } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift index ff50d3caf4..6d27aa60ea 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/TelegramEnginePayments.swift @@ -149,7 +149,7 @@ public extension TelegramEngine { return _internal_checkCanSendStarGift(account: self.account, giftId: giftId) } - public func getUniqueStarGift(slug: String) -> Signal { + public func getUniqueStarGift(slug: String) -> Signal { return _internal_getUniqueStarGift(account: self.account, slug: slug) } diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index eef5c0df08..1374de724b 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -1743,9 +1743,6 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = addAttributesToStringWithRanges(strings.Notification_StarsGiftOffer_Rejected(peerName, giftTitle, priceString)._tuple, body: bodyAttributes, argumentAttributes: attributes) } } - case .starGiftCraftFail: - //TODO:localize - attributedString = NSAttributedString(string: "Gift crafting failed", font: titleFont, textColor: primaryTextColor) case let .groupCreatorChange(groupCreatorChange): var targetName = "" if let peer = message.peers[groupCreatorChange.targetPeerId] { diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD index f5e828e899..c03b30d908 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD @@ -31,6 +31,9 @@ swift_library( "//submodules/TelegramUI/Components/AvatarComponent", "//submodules/Markdown", "//submodules/PhoneNumberFormat", + "//submodules/ContextUI", + "//submodules/AccountUtils", + "//submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AccountSwitchComponent.swift b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AccountSwitchComponent.swift new file mode 100644 index 0000000000..9d9eec5cb9 --- /dev/null +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AccountSwitchComponent.swift @@ -0,0 +1,134 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import GlassBackgroundComponent +import AvatarComponent +import BundleIconComponent +import AccountContext + +final class AccountSwitchComponent: Component { + let context: AccountContext + let theme: PresentationTheme + let peer: EnginePeer + let canSwitch: Bool + let action: ((GlassContextExtractableContainer) -> Void) + + init( + context: AccountContext, + theme: PresentationTheme, + peer: EnginePeer, + canSwitch: Bool, + action: @escaping ((GlassContextExtractableContainer) -> Void) + ) { + self.context = context + self.theme = theme + self.peer = peer + self.canSwitch = canSwitch + self.action = action + } + + static func ==(lhs: AccountSwitchComponent, rhs: AccountSwitchComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.canSwitch != rhs.canSwitch { + return false + } + return true + } + + final class View: UIView { + private let backgroundView = GlassContextExtractableContainer() + private let avatar = ComponentView() + private let arrow = ComponentView() + private let button = HighlightTrackingButton() + + private var component: AccountSwitchComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addSubview(self.backgroundView) + + self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func buttonPressed() { + if let component = self.component { + component.action(self.backgroundView) + } + } + + func update(component: AccountSwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let size = CGSize(width: component.canSwitch ? 76.0 : 44.0, height: 44.0) + + let avatarSize = self.avatar.update( + transition: .immediate, + component: AnyComponent( + AvatarComponent( + context: component.context, + theme: component.theme, + peer: component.peer, + ) + ), + environment: {}, + containerSize: CGSize(width: 36.0, height: 36.0) + ) + if let avatarView = self.avatar.view { + if avatarView.superview == nil { + avatarView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(avatarView) + } + avatarView.frame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: avatarSize) + } + + let arrowSize = self.arrow.update( + transition: .immediate, + component: AnyComponent( + BundleIconComponent(name: "Navigation/Disclosure", tintColor: component.theme.rootController.navigationBar.secondaryTextColor) + ), + environment: {}, + containerSize: availableSize + ) + if let arrowView = self.arrow.view { + if arrowView.superview == nil { + arrowView.isUserInteractionEnabled = false + self.backgroundView.contentView.addSubview(arrowView) + self.backgroundView.contentView.addSubview(self.button) + } + arrowView.frame = CGRect(origin: CGPoint(x: size.width - arrowSize.width - 12.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0)), size: arrowSize) + transition.setAlpha(view: arrowView, alpha: component.canSwitch ? 1.0 : 0.0) + } + + self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: component.canSwitch, transition: transition) + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: size)) + transition.setFrame(view: self.button, frame: CGRect(origin: .zero, size: size)) + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift index c796a2e4de..0f4daabd7f 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift @@ -19,6 +19,10 @@ import ListActionItemComponent import AvatarComponent import Markdown import PhoneNumberFormat +import ContextUI +import AccountUtils +import GlassBackgroundComponent +import AccountPeerContextItem private final class AuthConfirmationSheetContent: CombinedComponent { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -52,6 +56,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { private let subject: MessageActionUrlAuthResult var peer: EnginePeer? + var forcedAccount: (AccountContext, EnginePeer)? fileprivate var inProgress = false var allowWrite = true @@ -77,24 +82,24 @@ private final class AuthConfirmationSheetContent: CombinedComponent { guard case let .request(domain, _, _, _) = self.subject else { return } + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in guard let self, case let .user(user) = peer, let phone = user.phone else { return } - let phoneNumber = formatPhoneNumber(context: self.context, number: phone) + let phoneNumber = formatPhoneNumber(context: self.context, number: phone).replacingOccurrences(of: " ", with: "\u{00A0}") - //TODO:localize let alertController = textAlertController( context: self.context, - title: "Phone Number", - text: "**\(domain)** wants to access your phone number **\(phoneNumber)**.\n\nAllow access?", + title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Title, + text: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Text(domain, phoneNumber).string, actions: [ - TextAlertAction(type: .genericAction, title: "Deny", action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Deny, action: { commit(false) }), - TextAlertAction(type: .defaultAction, title: "Allow", action: { + TextAlertAction(type: .defaultAction, title: presentationData.strings.AuthConfirmation_PhoneNumberConfirmation_Allow, action: { commit(true) }) ] @@ -102,6 +107,51 @@ private final class AuthConfirmationSheetContent: CombinedComponent { self.controller?.present(alertController, in: .window(.root)) }) } + + func presentAccountSwitchMenu(sourceView: GlassContextExtractableContainer) { + guard let controller = self.controller else { + return + } + + let context = self.context + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let items: Signal<[ContextMenuItem], NoError> = activeAccountsAndPeers(context: self.context, includePrimary: true) + |> take(1) + |> map { primary, other -> [ContextMenuItem] in + var items: [ContextMenuItem] = [] + var existingIds = Set() + if let (_, peer) = primary { + items.append(.custom(AccountPeerContextItem(context: context, account: context.account, peer: peer, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + self.forcedAccount = nil + self.updated() + }), true)) + existingIds.insert(peer.id) + } + + for (accountContext, peer, _) in other { + guard !existingIds.contains(peer.id) else { + continue + } + items.append(.custom(AccountPeerContextItem(context: accountContext, account: accountContext.account, peer: peer, action: { [weak self] _, f in + f(.default) + guard let self else { + return + } + self.forcedAccount = (accountContext, peer) + self.updated() + }), true)) + } + + return items + } + + let contextController = makeContextController(presentationData: presentationData, source: .reference(AuthConfirmationReferenceContentSource(controller: controller, sourceView: sourceView)), items: items |> map { ContextController.Items(content: .list($0)) }, gesture: nil) + controller.presentInGlobalOverlay(contextController) + } } func makeState() -> State { @@ -110,7 +160,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { static var body: Body { let closeButton = Child(GlassBarButtonComponent.self) - let peerButton = Child(AvatarComponent.self) + let accountButton = Child(AccountSwitchComponent.self) let avatar = Child(AvatarComponent.self) let title = Child(MultilineTextComponent.self) let description = Child(MultilineTextComponent.self) @@ -162,17 +212,21 @@ private final class AuthConfirmationSheetContent: CombinedComponent { ) if let peer = state.peer { - let peerButton = peerButton.update( - component: AvatarComponent( - context: component.context, + let accountButton = accountButton.update( + component: AccountSwitchComponent( + context: state.forcedAccount?.0 ?? component.context, theme: environment.theme, - peer: peer + peer: state.forcedAccount?.1 ?? peer, + canSwitch: true, + action: { [weak state] sourceView in + state?.presentAccountSwitchMenu(sourceView: sourceView) + } ), - availableSize: CGSize(width: 44.0, height: 44.0), + availableSize: context.availableSize, transition: .immediate ) - context.add(peerButton - .position(CGPoint(x: context.availableSize.width - 16.0 - peerButton.size.width / 2.0, y: 16.0 + peerButton.size.height / 2.0)) + context.add(accountButton + .position(CGPoint(x: context.availableSize.width - 16.0 - accountButton.size.width / 2.0, y: 16.0 + accountButton.size.height / 2.0)) ) } @@ -192,11 +246,11 @@ private final class AuthConfirmationSheetContent: CombinedComponent { ) contentHeight += avatar.size.height contentHeight += 18.0 - + let titleFont = Font.bold(24.0) let title = title.update( component: MultilineTextComponent( - text: .markdown(text: "Log in to **\(domain)**", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), + text: .markdown(text: strings.AuthConfirmation_Title(domain).string, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.controlAccentColor), link: MarkdownAttributeSet(font: titleFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), horizontalAlignment: .center, maximumNumberOfLines: 2 ), @@ -213,7 +267,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { let boldTextFont = Font.semibold(15.0) let description = description.update( component: MultilineTextComponent( - text: .markdown(text: "This site will receive your **name**,\n**username** and **profile photo**.", attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), + text: .markdown(text: strings.AuthConfirmation_Description, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), bold: MarkdownAttributeSet(font: boldTextFont, textColor: theme.actionSheet.primaryTextColor), link: MarkdownAttributeSet(font: textFont, textColor: theme.actionSheet.primaryTextColor), linkAttribute: { _ in return nil })), horizontalAlignment: .center, maximumNumberOfLines: 3, lineSpacing: 0.2 @@ -235,7 +289,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { style: .glass, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Device", + string: strings.AuthConfirmation_Device, font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor )), @@ -283,7 +337,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { style: .glass, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "IP Address", + string: strings.AuthConfirmation_IpAddress, font: Font.regular(17.0), textColor: theme.list.itemPrimaryTextColor )), @@ -331,7 +385,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "This login attempt came from the device above.", + string: strings.AuthConfirmation_Info, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -357,7 +411,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Allow Messages", + string: strings.AuthConfirmation_AllowMessages, font: Font.regular(presentationData.listsFontSize.itemListBaseFontSize), textColor: theme.list.itemPrimaryTextColor )), @@ -380,7 +434,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { header: nil, footer: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "This will allow \(EnginePeer(bot).compactDisplayTitle) to message you.", + string: strings.AuthConfirmation_AllowMessagesInfo(EnginePeer(bot).compactDisplayTitle).string, font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor )), @@ -412,7 +466,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Cancel", font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.AuthConfirmation_Cancel, font: Font.semibold(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)))) ), action: { component.cancel(true) @@ -436,7 +490,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent { ), content: AnyComponentWithIdentity( id: AnyHashable(0), - component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: "Log In", font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) + component: AnyComponent(MultilineTextComponent(text: .plain(NSMutableAttributedString(string: strings.AuthConfirmation_LogIn, font: Font.semibold(17.0), textColor: theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)))) ), displaysProgress: state.inProgress, action: { [weak state] in @@ -610,3 +664,19 @@ public class AuthConfirmationScreen: ViewControllerComponentContainer { } } } + +private final class AuthConfirmationReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + + let forceDisplayBelowKeyboard = true + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) + } +} diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift index 12e1c48300..d3e7eb48db 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGiftBubbleContentNode/Sources/ChatMessageGiftBubbleContentNode.swift @@ -725,6 +725,11 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode { } else { ribbonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_Gift } + + if uniqueGift.flags.contains(.isBurned) { + ribbonTitle = item.presentationData.strings.Notification_StarGift_Burned + } + buttonTitle = isStoryEntity ? "" : item.presentationData.strings.Notification_StarGift_View modelTitle = item.presentationData.strings.Notification_StarGift_Model backdropTitle = item.presentationData.strings.Notification_StarGift_Backdrop diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift index a6a3bff6eb..9ad2e05c0f 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/CraftTableComponent.swift @@ -299,7 +299,7 @@ final class CraftTableComponent: Component { diameter: 84.0, lineWidth: 5.0, fontSize: 18.0, - progress: CGFloat(permilleValue / 10 / 100), + progress: CGFloat(permilleValue) / 10.0 / 100.0, value: permilleValue / 10, suffix: "%", isVisible: !component.isCrafting diff --git a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift index bdcc2ad5dc..82682af122 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftCraftScreen/Sources/GiftCraftScreen.swift @@ -460,7 +460,7 @@ private final class CraftGiftPageContent: Component { }, containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) ) - craftContentHeight += 276.0 + craftContentHeight += 291.0 craftContentHeight += descriptionTextSize.height craftContentHeight += 25.0 @@ -566,7 +566,7 @@ private final class CraftGiftPageContent: Component { diameter: 48.0, lineWidth: 4.0, fontSize: 10.0, - progress: CGFloat(backdropPermille / 10 / 100), + progress: CGFloat(backdropPermille) / 10.0 / 100.0, value: backdropPermille / 10, suffix: "%" ) @@ -615,7 +615,7 @@ private final class CraftGiftPageContent: Component { contentSize: CGSize(width: 28.0, height: 28.0), lineWidth: 4.0, fontSize: 10.0, - progress: CGFloat(symbolPermille / 10 / 100), + progress: CGFloat(symbolPermille) / 10.0 / 100.0, value: symbolPermille / 10, suffix: "%" ) diff --git a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift index 73be35b2dd..a27c6aa641 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftStoreScreen/Sources/GiftStoreScreen.swift @@ -1220,7 +1220,7 @@ final class GiftStoreScreenComponent: Component { } let tonBalance = tonContext.currentState?.balance.value ?? 0 if tonBalance == 0 { - let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: tonContext) + let controller = component.context.sharedContext.makeStarsTransactionsScreen(context: component.context, starsContext: starsContext) controller.push(controller) return } @@ -1242,7 +1242,7 @@ final class GiftStoreScreenComponent: Component { var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem( - text: "My Stars", + text: presentationData.strings.Gift_Store_Balance_MyStars, textLayout: .secondLineWithValue(formatStarsAmountText(starsBalance, dateTimeFormat: presentationData.dateTimeFormat)), icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stars"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in @@ -1256,7 +1256,7 @@ final class GiftStoreScreenComponent: Component { ))) items.append(.action(ContextMenuActionItem( - text: "My TON", + text: presentationData.strings.Gift_Store_Balance_MyTon, textLayout: .secondLineWithValue(formatTonAmountText(tonBalance, dateTimeFormat: presentationData.dateTimeFormat)), icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Ton"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index 0c3ed41f00..4c1e3a36d9 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -1013,6 +1013,7 @@ private final class GiftViewSheetContent: CombinedComponent { .single(gift.themePeerId) |> then( context.engine.payments.getUniqueStarGift(slug: gift.slug) + |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } diff --git a/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/BUILD b/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/BUILD new file mode 100644 index 0000000000..074697e8d6 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/BUILD @@ -0,0 +1,29 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "AccountPeerContextItem", + module_name = "AccountPeerContextItem", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/PresentationDataUtils", + "//submodules/TelegramPresentationData", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/AvatarNode", + "//submodules/ContextUI", + "//submodules/AccountContext", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/Sources/AccountPeerContextItem.swift b/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/Sources/AccountPeerContextItem.swift new file mode 100644 index 0000000000..69e5feddb0 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem/Sources/AccountPeerContextItem.swift @@ -0,0 +1,174 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import ComponentFlow +import TelegramCore +import TelegramPresentationData +import PresentationDataUtils +import ContextUI +import AvatarNode +import EmojiStatusComponent +import AccountContext + +public final class AccountPeerContextItem: ContextMenuCustomItem { + let context: AccountContext + let account: Account + let peer: EnginePeer + let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void + + public init(context: AccountContext, account: Account, peer: EnginePeer, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { + self.context = context + self.account = account + self.peer = peer + self.action = action + } + + public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { + return AccountPeerContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) + } +} + +private final class AccountPeerContextItemNode: ASDisplayNode, ContextMenuCustomNode { + private let item: AccountPeerContextItem + private let presentationData: PresentationData + private let getController: () -> ContextControllerProtocol? + private let actionSelected: (ContextMenuActionResult) -> Void + + private let buttonNode: HighlightTrackingButtonNode + private let textNode: ImmediateTextNode + private let avatarNode: AvatarNode + private let emojiStatusView: ComponentView + + init(presentationData: PresentationData, item: AccountPeerContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { + self.item = item + self.presentationData = presentationData + self.getController = getController + self.actionSelected = actionSelected + + let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 17.0 / 17.0) + + self.textNode = ImmediateTextNode() + self.textNode.isAccessibilityElement = false + self.textNode.isUserInteractionEnabled = false + self.textNode.displaysAsynchronously = false + let peerTitle = item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + self.textNode.attributedText = NSAttributedString(string: peerTitle, font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) + self.textNode.maximumNumberOfLines = 1 + + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + + self.emojiStatusView = ComponentView() + + self.buttonNode = HighlightTrackingButtonNode() + self.buttonNode.isAccessibilityElement = true + self.buttonNode.accessibilityLabel = peerTitle + + super.init() + + self.addSubnode(self.textNode) + self.addSubnode(self.avatarNode) + self.addSubnode(self.buttonNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) + } + + func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { + let sideInset: CGFloat = 18.0 + let iconSideInset: CGFloat = 20.0 + let verticalInset: CGFloat = 11.0 + + let iconSize = CGSize(width: 28.0, height: 28.0) + + let standardIconWidth: CGFloat = 32.0 + var rightTextInset: CGFloat = sideInset + if !iconSize.width.isZero { + rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset - 12.0 + } + + self.avatarNode.setPeer(context: self.item.context, account: self.item.account, theme: self.presentationData.theme, peer: self.item.peer) + + if self.item.peer.emojiStatus != nil { + rightTextInset += 32.0 + } + + let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) + + return (CGSize(width: textSize.width + sideInset + rightTextInset, height: verticalInset * 2.0 + textSize.height), { size, transition in + let verticalOrigin = floor((size.height - textSize.height) / 2.0) + let textFrame = CGRect(origin: CGPoint(x: iconSideInset + 40.0, y: verticalOrigin), size: textSize) + transition.updateFrameAdditive(node: self.textNode, frame: textFrame) + + var iconContent: EmojiStatusComponent.Content? + if case let .user(user) = self.item.peer { + if let emojiStatus = user.emojiStatus { + iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever) + } else if user.isPremium { + iconContent = .premium(color: self.presentationData.theme.list.itemAccentColor) + } + } else if case let .channel(channel) = self.item.peer { + if let emojiStatus = channel.emojiStatus { + iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever) + } + } + if let iconContent { + let emojiStatusSize = self.emojiStatusView.update( + transition: .immediate, + component: AnyComponent(EmojiStatusComponent( + context: self.item.context, + animationCache: self.item.context.animationCache, + animationRenderer: self.item.context.animationRenderer, + content: iconContent, + isVisibleForAnimations: true, + action: nil + )), + environment: {}, + containerSize: CGSize(width: 24.0, height: 24.0) + ) + if let view = self.emojiStatusView.view { + if view.superview == nil { + self.view.addSubview(view) + } + transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: textFrame.maxX + 2.0, y: textFrame.minY + floor((textFrame.height - emojiStatusSize.height) / 2.0)), size: emojiStatusSize)) + } + } + + transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) + + transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + }) + } + + func updateTheme(presentationData: PresentationData) { + if let attributedText = self.textNode.attributedText { + let updatedAttributedText = NSMutableAttributedString(attributedString: attributedText) + updatedAttributedText.addAttribute(.foregroundColor, value: presentationData.theme.contextMenu.primaryColor.cgColor, range: NSRange(location: 0, length: updatedAttributedText.length)) + self.textNode.attributedText = updatedAttributedText + } + } + + @objc private func buttonPressed() { + self.performAction() + } + + func canBeHighlighted() -> Bool { + return true + } + + func setIsHighlighted(_ value: Bool) { + } + + func updateIsHighlighted(isHighlighted: Bool) { + self.setIsHighlighted(isHighlighted) + } + + func performAction() { + guard let controller = self.getController() else { + return + } + self.item.action(controller, { [weak self] result in + self?.actionSelected(result) + }) + } +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD index 532cde919e..c35771e65c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/BUILD @@ -173,6 +173,7 @@ swift_library( "//submodules/TelegramUI/Components/AvatarComponent", "//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent", "//submodules/TelegramUI/Components/HorizontalTabsComponent", + "//submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 89a727c278..14075eb95a 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -114,6 +114,7 @@ import GiftViewScreen import PeerMessagesMediaPlaylist import EdgeEffect import Pasteboard +import AccountPeerContextItem public enum PeerInfoAvatarEditingMode { case generic diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift index b0628e90b5..3bf7a5f4e4 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsTabActions.swift @@ -89,167 +89,6 @@ extension PeerInfoScreenNode { } } -final class AccountPeerContextItem: ContextMenuCustomItem { - let context: AccountContext - let account: Account - let peer: EnginePeer - let action: (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void - - init(context: AccountContext, account: Account, peer: EnginePeer, action: @escaping (ContextControllerProtocol, @escaping (ContextMenuActionResult) -> Void) -> Void) { - self.context = context - self.account = account - self.peer = peer - self.action = action - } - - public func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode { - return AccountPeerContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected) - } -} - -private final class AccountPeerContextItemNode: ASDisplayNode, ContextMenuCustomNode { - private let item: AccountPeerContextItem - private let presentationData: PresentationData - private let getController: () -> ContextControllerProtocol? - private let actionSelected: (ContextMenuActionResult) -> Void - - private let buttonNode: HighlightTrackingButtonNode - private let textNode: ImmediateTextNode - private let avatarNode: AvatarNode - private let emojiStatusView: ComponentView - - init(presentationData: PresentationData, item: AccountPeerContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) { - self.item = item - self.presentationData = presentationData - self.getController = getController - self.actionSelected = actionSelected - - let textFont = Font.regular(presentationData.listsFontSize.baseDisplaySize * 17.0 / 17.0) - - self.textNode = ImmediateTextNode() - self.textNode.isAccessibilityElement = false - self.textNode.isUserInteractionEnabled = false - self.textNode.displaysAsynchronously = false - let peerTitle = item.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) - self.textNode.attributedText = NSAttributedString(string: peerTitle, font: textFont, textColor: presentationData.theme.contextMenu.primaryColor) - self.textNode.maximumNumberOfLines = 1 - - self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) - - self.emojiStatusView = ComponentView() - - self.buttonNode = HighlightTrackingButtonNode() - self.buttonNode.isAccessibilityElement = true - self.buttonNode.accessibilityLabel = peerTitle - - super.init() - - self.addSubnode(self.textNode) - self.addSubnode(self.avatarNode) - self.addSubnode(self.buttonNode) - - self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) - } - - func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { - let sideInset: CGFloat = 18.0 - let iconSideInset: CGFloat = 20.0 - let verticalInset: CGFloat = 11.0 - - let iconSize = CGSize(width: 28.0, height: 28.0) - - let standardIconWidth: CGFloat = 32.0 - var rightTextInset: CGFloat = sideInset - if !iconSize.width.isZero { - rightTextInset = max(iconSize.width, standardIconWidth) + iconSideInset + sideInset - 12.0 - } - - self.avatarNode.setPeer(context: self.item.context, account: self.item.account, theme: self.presentationData.theme, peer: self.item.peer) - - if self.item.peer.emojiStatus != nil { - rightTextInset += 32.0 - } - - let textSize = self.textNode.updateLayout(CGSize(width: constrainedWidth - sideInset - rightTextInset, height: .greatestFiniteMagnitude)) - - return (CGSize(width: textSize.width + sideInset + rightTextInset, height: verticalInset * 2.0 + textSize.height), { size, transition in - let verticalOrigin = floor((size.height - textSize.height) / 2.0) - let textFrame = CGRect(origin: CGPoint(x: iconSideInset + 40.0, y: verticalOrigin), size: textSize) - transition.updateFrameAdditive(node: self.textNode, frame: textFrame) - - var iconContent: EmojiStatusComponent.Content? - if case let .user(user) = self.item.peer { - if let emojiStatus = user.emojiStatus { - iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever) - } else if user.isPremium { - iconContent = .premium(color: self.presentationData.theme.list.itemAccentColor) - } - } else if case let .channel(channel) = self.item.peer { - if let emojiStatus = channel.emojiStatus { - iconContent = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 28.0, height: 28.0), placeholderColor: self.presentationData.theme.list.mediaPlaceholderColor, themeColor: self.presentationData.theme.list.itemAccentColor, loopMode: .forever) - } - } - if let iconContent { - let emojiStatusSize = self.emojiStatusView.update( - transition: .immediate, - component: AnyComponent(EmojiStatusComponent( - context: self.item.context, - animationCache: self.item.context.animationCache, - animationRenderer: self.item.context.animationRenderer, - content: iconContent, - isVisibleForAnimations: true, - action: nil - )), - environment: {}, - containerSize: CGSize(width: 24.0, height: 24.0) - ) - if let view = self.emojiStatusView.view { - if view.superview == nil { - self.view.addSubview(view) - } - transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: textFrame.maxX + 2.0, y: textFrame.minY + floor((textFrame.height - emojiStatusSize.height) / 2.0)), size: emojiStatusSize)) - } - } - - transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: iconSideInset + floor((standardIconWidth - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)) - - transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) - }) - } - - func updateTheme(presentationData: PresentationData) { - if let attributedText = self.textNode.attributedText { - let updatedAttributedText = NSMutableAttributedString(attributedString: attributedText) - updatedAttributedText.addAttribute(.foregroundColor, value: presentationData.theme.contextMenu.primaryColor.cgColor, range: NSRange(location: 0, length: updatedAttributedText.length)) - self.textNode.attributedText = updatedAttributedText - } - } - - @objc private func buttonPressed() { - self.performAction() - } - - func canBeHighlighted() -> Bool { - return true - } - - func setIsHighlighted(_ value: Bool) { - } - - func updateIsHighlighted(isHighlighted: Bool) { - self.setIsHighlighted(isHighlighted) - } - - func performAction() { - guard let controller = self.getController() else { - return - } - self.item.action(controller, { [weak self] result in - self?.actionSelected(result) - }) - } -} - final class PeerInfoControllerContextReferenceContentSource: ContextReferenceContentSource { let controller: ViewController let sourceView: UIView diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/Disclosure.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Navigation/Disclosure.imageset/Contents.json new file mode 100644 index 0000000000..6f5603fca5 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Navigation/Disclosure.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "openicon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Navigation/Disclosure.imageset/openicon.pdf b/submodules/TelegramUI/Images.xcassets/Navigation/Disclosure.imageset/openicon.pdf new file mode 100644 index 0000000000..6d76f47363 Binary files /dev/null and b/submodules/TelegramUI/Images.xcassets/Navigation/Disclosure.imageset/openicon.pdf differ diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index a0f5eb924f..4e75423c5d 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1632,33 +1632,36 @@ func openResolvedUrlImpl( present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } }) - case let .collectible(gift): - if let gift { - var dismissedImpl: (() -> Void)? - if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { - let updateExternalController = storyProgressPauseContext.update - dismissedImpl = { - updateExternalController(nil) - } + case let .collectible(result): + switch result { + case let .gift(gift): + var dismissedImpl: (() -> Void)? + if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { + let updateExternalController = storyProgressPauseContext.update + dismissedImpl = { + updateExternalController(nil) } - let controller = context.sharedContext.makeGiftViewScreen(context: context, gift: gift, shareStory: { [weak navigationController] uniqueGift in - Queue.mainQueue().after(0.15) { - if let lastController = navigationController?.viewControllers.last as? ViewController { - let controller = context.sharedContext.makeStorySharingScreen(context: context, subject: .gift(gift), parentController: lastController) - navigationController?.pushViewController(controller) - } - } - }, openChatTheme: nil, dismissed: { - dismissedImpl?() - }) - navigationController?.pushViewController(controller) - - if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { - storyProgressPauseContext.update(controller) - } - } else { - present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) } + let controller = context.sharedContext.makeGiftViewScreen(context: context, gift: gift, shareStory: { [weak navigationController] uniqueGift in + Queue.mainQueue().after(0.15) { + if let lastController = navigationController?.viewControllers.last as? ViewController { + let controller = context.sharedContext.makeStorySharingScreen(context: context, subject: .gift(gift), parentController: lastController) + navigationController?.pushViewController(controller) + } + } + }, openChatTheme: nil, dismissed: { + dismissedImpl?() + }) + navigationController?.pushViewController(controller) + + if let storyProgressPauseContext = contentContext as? StoryProgressPauseContext { + storyProgressPauseContext.update(controller) + } + case .alreadyBurned: + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Resolve_GiftErrorBurned, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + case .invalidSlug: + present(textAlertController(context: context, updatedPresentationData: updatedPresentationData, title: nil, text: presentationData.strings.Resolve_GiftErrorNotFound, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil) + } case let .auction(auctionContext): if let auctionContext, case let .generic(gift) = auctionContext.gift { if !auctionContext.isFinished, let currentBidPeerId = auctionContext.currentBidPeerId { @@ -1818,8 +1821,27 @@ func openResolvedUrlImpl( if case .request = result { var dismissImpl: (() -> Void)? let controller = AuthConfirmationScreen(context: context, subject: result, completion: { allowWriteAccess, sharePhoneNumber in - let _ = context.engine.messages.acceptMessageActionUrlAuth(subject: .url(url), allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber).start(next: { _ in + let _ = (context.engine.messages.acceptMessageActionUrlAuth(subject: .url(url), allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber) + |> deliverOnMainQueue).start(next: { _ in dismissImpl?() + + Queue.mainQueue().after(0.3) { + let text: String + if case let .request(domain, _, _, flags) = result { + if flags.contains(.requestPhoneNumber) && !sharePhoneNumber { + text = presentationData.strings.AuthConfirmation_LoginSuccess_TextNoNumber(domain).string + } else { + text = presentationData.strings.AuthConfirmation_LoginSuccess_Text(domain).string + } + let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AuthConfirmation_LoginSuccess_Title, text: text, cancel: nil, destructive: false), action: { _ in return true }) + (navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root)) + } + } + }, error: { _ in + if case let .request(domain, _, _, _) = result { + let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AuthConfirmation_LoginFail_Title, text: presentationData.strings.AuthConfirmation_LoginFail_Text(domain).string, cancel: nil, destructive: false), action: { _ in return true }) + (navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root)) + } }) }) navigationController?.pushViewController(controller) diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 614aef7988..b2469ebd3f 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -846,7 +846,14 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) if let peer { return .single(peer) } else { - return context.engine.peers.findChannelById(channelId: monoforumId.id._internalGetInt64Value()) + return context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id) + |> mapToSignal { result -> Signal in + if result { + return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: monoforumId)) + } else { + return .single(nil) + } + } } } |> map { peer -> ResolveInternalUrlResult in @@ -1208,12 +1215,17 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) return .single(.result(.premiumGiftCode(slug: slug))) case let .collectible(slug): return .single(.progress) |> then(context.engine.payments.getUniqueStarGift(slug: slug) - |> `catch` { _ -> Signal in - return .single(nil) - } |> map { gift -> ResolveInternalUrlResult in - return .result(.collectible(gift: gift)) + return .result(.collectible(.gift(gift))) }) + |> `catch` { error -> Signal in + switch error { + case .alreadyBurned: + return .single(.result(.collectible(.alreadyBurned))) + default: + return .single(.result(.collectible(.invalidSlug))) + } + } case let .auction(slug): if let giftAuctionsManager = context.giftAuctionsManager { return .single(.progress) |> then(giftAuctionsManager.auctionContext(for: .slug(slug))