This commit is contained in:
Isaac 2026-01-30 23:53:59 +08:00
commit 37ffcf76e9
26 changed files with 590 additions and 254 deletions

View file

@ -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$@.";

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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<MessageActionUrlAuthResult, NoError> {
func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal<MessageActionUrlAuthResult, MessageActionUrlAuthError> {
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<Api.UrlAuthResult?, NoError> 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

View file

@ -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<MessageActionUrlAuthResult, NoError> {
public func acceptMessageActionUrlAuth(subject: MessageActionUrlSubject, allowWriteAccess: Bool, sharePhoneNumber: Bool) -> Signal<MessageActionUrlAuthResult, MessageActionUrlAuthError> {
return _internal_acceptMessageActionUrlAuth(account: self.account, subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber)
}

View file

@ -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<StarGift.UniqueGift?, GetUniqueStarGiftError> {
func _internal_getUniqueStarGift(account: Account, slug: String) -> Signal<StarGift.UniqueGift, GetUniqueStarGiftError> {
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<StarGift.UniqueGift?, GetUniqueStarGiftError> in
|> mapToSignal { result -> Signal<StarGift.UniqueGift, GetUniqueStarGiftError> 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<StarGift.UniqueGift, GetUniqueStarGiftError> 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
}
}
}

View file

@ -149,7 +149,7 @@ public extension TelegramEngine {
return _internal_checkCanSendStarGift(account: self.account, giftId: giftId)
}
public func getUniqueStarGift(slug: String) -> Signal<StarGift.UniqueGift?, GetUniqueStarGiftError> {
public func getUniqueStarGift(slug: String) -> Signal<StarGift.UniqueGift, GetUniqueStarGiftError> {
return _internal_getUniqueStarGift(account: self.account, slug: slug)
}

View file

@ -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] {

View file

@ -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",

View file

@ -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<Empty>()
private let arrow = ComponentView<Empty>()
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<Empty>, 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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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<EnginePeer.Id>()
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)
}
}

View file

@ -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

View file

@ -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

View file

@ -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: "%"
)

View file

@ -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

View file

@ -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<StarGift.UniqueGift?, NoError> in
return .single(nil)
}

View file

@ -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",
],
)

View file

@ -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<Empty>
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<Empty>()
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)
})
}
}

View file

@ -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",

View file

@ -114,6 +114,7 @@ import GiftViewScreen
import PeerMessagesMediaPlaylist
import EdgeEffect
import Pasteboard
import AccountPeerContextItem
public enum PeerInfoAvatarEditingMode {
case generic

View file

@ -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<Empty>
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<Empty>()
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

View file

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

View file

@ -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)

View file

@ -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<EnginePeer?, NoError> 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<StarGift.UniqueGift?, NoError> in
return .single(nil)
}
|> map { gift -> ResolveInternalUrlResult in
return .result(.collectible(gift: gift))
return .result(.collectible(.gift(gift)))
})
|> `catch` { error -> Signal<ResolveInternalUrlResult, NoError> 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))