Various improvements

This commit is contained in:
Ilya Laktyushin 2026-02-24 21:44:29 +04:00
parent 69c8e2634b
commit 654ff9e7e0
8 changed files with 216 additions and 60 deletions

View file

@ -441,7 +441,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
return
}
var dismissImpl: (() -> Void)?
let controller = AuthConfirmationScreen(context: self.context, subject: result, completion: { [weak self] accountContext, accountPeer, authResult in
let controller = AuthConfirmationScreen(context: self.context, requestSubject: subject, subject: result, completion: { [weak self] accountContext, accountPeer, authResult in
guard let self else {
return
}
@ -503,6 +503,19 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
case .decline:
let _ = self.context.engine.messages.declineUrlAuth(url: url).start()
self.webView.sendEvent(name: "oauth_result_failed", data: nil)
case .failed:
guard case let .request(domain, _, _, _, _, _) = result else {
return
}
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 })
if let navigationController = self.getNavigationController() {
(navigationController.topViewController as? ViewController)?.present(controller, in: .window(.root))
}
let _ = self.context.engine.messages.declineUrlAuth(url: url).start()
self.webView.sendEvent(name: "oauth_result_failed", data: nil)
HapticFeedback().error()
}
})
dismissImpl = { [weak controller] in

View file

@ -102,8 +102,8 @@ private func allOpenInOptions(context: AccountContext, item: OpenInItem) -> [Ope
if !skipSafari {
options.append(OpenInOption(identifier: "safari", application: .safari, action: {
var url = url
if url.hasPrefix("http://") || url.hasPrefix("https://") {
url = url.replacingOccurrences(of: "https://", with: "x-safari-https")
if url.hasPrefix("https://") {
url = url.replacingOccurrences(of: "https://", with: "x-safari-https://")
}
return .openUrl(url: url)
}))

View file

@ -5322,6 +5322,22 @@ public extension Api.functions.messages {
})
}
}
public extension Api.functions.messages {
static func checkUrlAuthMatchCode(url: String, matchCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(-911967477)
serializeString(url, buffer: buffer, boxed: false)
serializeString(matchCode, buffer: buffer, boxed: false)
return (FunctionDescription(name: "messages.checkUrlAuthMatchCode", parameters: [("url", String(describing: url)), ("matchCode", String(describing: matchCode))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public extension Api.functions.messages {
static func clearAllDrafts() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()

View file

@ -178,6 +178,7 @@ public enum MessageActionUrlAuthResult {
public static let requestWriteAccess = Flags(rawValue: 1 << 0)
public static let requestPhoneNumber = Flags(rawValue: 1 << 1)
public static let showMatchCodesFirst = Flags(rawValue: 1 << 2)
}
public struct ClientData : Equatable {
@ -244,25 +245,29 @@ func _internal_requestMessageActionUrlAuth(account: Account, subject: MessageAct
return .default
}
switch result {
case .urlAuthResultDefault:
return .default
case let .urlAuthResultAccepted(urlAuthResultAcceptedData):
let url = urlAuthResultAcceptedData.url
return .accepted(url: url)
case let .urlAuthResultRequest(urlAuthResultRequestData):
let (apiFlags, bot, domain) = (urlAuthResultRequestData.flags, urlAuthResultRequestData.bot, urlAuthResultRequestData.domain)
var clientData: MessageActionUrlAuthResult.ClientData?
if let browser = urlAuthResultRequestData.browser, let platform = urlAuthResultRequestData.platform, let ip = urlAuthResultRequestData.ip, let region = urlAuthResultRequestData.region {
clientData = MessageActionUrlAuthResult.ClientData(browser: browser, platform: platform, ip: ip, region: region)
}
var flags: MessageActionUrlAuthResult.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.requestWriteAccess)
}
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.requestPhoneNumber)
}
return .request(domain: domain, bot: TelegramUser(user: bot), clientData: clientData, flags: flags, matchCodes: urlAuthResultRequestData.matchCodes, userIdHint: urlAuthResultRequestData.userIdHint.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) })
case .urlAuthResultDefault:
return .default
case let .urlAuthResultAccepted(urlAuthResultAcceptedData):
let url = urlAuthResultAcceptedData.url
return .accepted(url: url)
case let .urlAuthResultRequest(urlAuthResultRequestData):
let (apiFlags, bot, domain) = (urlAuthResultRequestData.flags, urlAuthResultRequestData.bot, urlAuthResultRequestData.domain)
var clientData: MessageActionUrlAuthResult.ClientData?
if let browser = urlAuthResultRequestData.browser, let platform = urlAuthResultRequestData.platform, let ip = urlAuthResultRequestData.ip, let region = urlAuthResultRequestData.region {
clientData = MessageActionUrlAuthResult.ClientData(browser: browser, platform: platform, ip: ip, region: region)
}
var flags: MessageActionUrlAuthResult.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.requestWriteAccess)
}
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.requestPhoneNumber)
}
if (apiFlags & (1 << 5)) != 0 {
flags.insert(.showMatchCodesFirst)
}
return .request(domain: domain, bot: TelegramUser(user: bot), clientData: clientData, flags: flags, matchCodes: urlAuthResultRequestData.matchCodes, userIdHint: urlAuthResultRequestData.userIdHint.flatMap { EnginePeer.Id(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) })
}
}
}
@ -327,3 +332,13 @@ func _internal_acceptMessageActionUrlAuth(account: Account, subject: MessageActi
}
}
}
func _internal_checkUrlAuthMatchCode(account: Account, url: String, matchCode: String) -> Signal<Bool, NoError> {
return account.network.request(Api.functions.messages.checkUrlAuthMatchCode(url: url, matchCode: matchCode))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> map { result in
return result == .boolTrue
}
}

View file

@ -106,6 +106,10 @@ public extension TelegramEngine {
public func declineUrlAuth(url: String) -> Signal<Never, NoError> {
return _internal_declineUrlAuth(account: self.account, url: url)
}
public func checkUrlAuthMatchCode(url: String, matchCode: String) -> Signal<Bool, NoError> {
return _internal_checkUrlAuthMatchCode(account: self.account, url: url, matchCode: matchCode)
}
public func searchMessages(location: SearchMessagesLocation, query: String, state: SearchMessagesState?, centerId: MessageId? = nil, limit: Int32 = 100) -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> {
return _internal_searchMessages(account: self.account, location: location, query: query, state: state, centerId: centerId, limit: limit)

View file

@ -36,6 +36,8 @@ swift_library(
"//submodules/AccountUtils",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/PeerInfo/AccountPeerContextItem",
"//submodules/TelegramUI/Components/LottieComponent",
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
],
visibility = [
"//visibility:public",

View file

@ -25,22 +25,27 @@ import AccountUtils
import GlassBackgroundComponent
import AccountPeerContextItem
import ActivityIndicator
import LottieComponent
import LottieComponentResourceContent
private final class AuthConfirmationSheetContent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let requestSubject: MessageActionUrlSubject
let subject: MessageActionUrlAuthResult
let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
let cancel: (Bool) -> Void
init(
context: AccountContext,
requestSubject: MessageActionUrlSubject,
subject: MessageActionUrlAuthResult,
completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void,
cancel: @escaping (Bool) -> Void
) {
self.context = context
self.requestSubject = requestSubject
self.subject = subject
self.completion = completion
self.cancel = cancel
@ -55,7 +60,9 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
final class State: ComponentState {
private let context: AccountContext
private let requestSubject: MessageActionUrlSubject
private let subject: MessageActionUrlAuthResult
private let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
var peer: EnginePeer?
var forcedAccount: (AccountContext, EnginePeer)?
@ -68,9 +75,11 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
var matchCodes: [String]?
var selectedMatchCode: String?
init(context: AccountContext, subject: MessageActionUrlAuthResult) {
init(context: AccountContext, requestSubject: MessageActionUrlSubject, subject: MessageActionUrlAuthResult, completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void) {
self.context = context
self.requestSubject = requestSubject
self.subject = subject
self.completion = completion
super.init()
@ -82,6 +91,65 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
self.peer = peer
self.updated()
})
if case let .request(_, _, _, flags, matchCodes, _) = self.subject, let matchCodes, flags.contains(.showMatchCodesFirst) {
self.displayEmoji = true
self.matchCodes = matchCodes.shuffled()
}
if case let .request(_, _, _, _, _, userIdHint) = self.subject, let userIdHint, userIdHint != context.account.peerId {
let _ = (activeAccountsAndPeers(context: self.context, includePrimary: true)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] primary, other in
guard let self else {
return
}
for (accountContext, peer, _) in other {
if peer.id == userIdHint {
self.forcedAccount = (accountContext, peer)
self.updated()
accountContext.account.shouldBeServiceTaskMaster.set(.single(.now))
let _ = accountContext.engine.messages.requestMessageActionUrlAuth(subject: requestSubject).start()
break
}
}
})
}
}
deinit {
if !self.inProgress {
if let (context, _) = self.forcedAccount {
context.account.shouldBeServiceTaskMaster.set(.single(.never))
}
}
}
func checkMatchCode(_ matchCode: String) {
guard case let .url(url, _) = self.requestSubject else {
return
}
self.selectedMatchCode = matchCode
self.updated(transition: .easeInOut(duration: 0.2))
let _ = (self.context.engine.messages.checkUrlAuthMatchCode(url: url, matchCode: matchCode)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
if result {
self.displayEmoji = false
self.updated(transition: .spring(duration: 0.4))
} else {
let accountContext = self.forcedAccount?.0 ?? self.context
guard let accountPeer = self.forcedAccount?.1 ?? self.peer else {
return
}
self.completion(accountContext, accountPeer, .failed)
}
})
}
func displayPhoneNumberConfirmation(commit: @escaping (Bool) -> Void) {
@ -132,6 +200,10 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
guard let self else {
return
}
if let (context, _) = self.forcedAccount {
context.account.shouldBeServiceTaskMaster.set(.single(.never))
}
self.forcedAccount = nil
self.updated()
}), true))
@ -147,6 +219,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
guard let self else {
return
}
if let (context, _) = self.forcedAccount {
context.account.shouldBeServiceTaskMaster.set(.single(.never))
}
accountContext.account.shouldBeServiceTaskMaster.set(.single(.now))
self.forcedAccount = (accountContext, peer)
self.updated()
}), true))
@ -161,7 +239,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
}
func makeState() -> State {
return State(context: self.context, subject: self.subject)
return State(context: self.context, requestSubject: self.requestSubject, subject: self.subject, completion: self.completion)
}
static var body: Body {
@ -191,13 +269,10 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
}
let presentationData = context.component.context.sharedContext.currentPresentationData.with { $0 }
let _ = strings
guard case let .request(domain, bot, clientData, flags, matchCodes, userIdHint) = component.subject else {
guard case let .request(domain, bot, clientData, flags, matchCodes, _) = component.subject else {
fatalError()
}
let _ = userIdHint
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
@ -241,7 +316,6 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
)
context.add(accountButton
.position(CGPoint(x: context.availableSize.width - 16.0 - accountButton.size.width / 2.0, y: 16.0 + accountButton.size.height / 2.0))
.scale(state.displayEmoji ? 0.0 : 1.0)
)
}
@ -338,12 +412,28 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
))
)
}
items.append(
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
Text(text: code, font: Font.regular(32.0), color: .black)
))
)
var file: TelegramMediaFile?
if let item = component.context.animatedEmojiStickersValue[code] {
file = item.first?.file._parse()
} else if let item = component.context.animatedEmojiStickersValue[code.strippedEmoji] {
file = item.first?.file._parse()
}
if let file {
items.append(
AnyComponentWithIdentity(id: "animatedIcon", component: AnyComponent(
LottieComponent(content: LottieComponent.ResourceContent(context: component.context, file: file, attemptSynchronously: true, providesPlaceholder: true), placeholderColor: theme.list.mediaPlaceholderColor, startingPosition: .begin, size: CGSize(width: 32.0, height: 32.0), loop: true, playOnce: nil)
))
)
} else {
items.append(
AnyComponentWithIdentity(id: "staticIcon", component: AnyComponent(
Text(text: code, font: Font.regular(32.0), color: .black)
))
)
}
let subject = component.subject
let emoji = emojis[code].update(
component: AnyComponent(
PlainButtonComponent(
@ -351,9 +441,17 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
ZStack(items)
),
minSize: emojiSize,
action: {
complete(code)
action: { [weak state] in
guard let state else {
return
}
if case let .request(_, _, _, flags, _, _) = subject, flags.contains(.showMatchCodesFirst) {
state.checkMatchCode(code)
} else {
complete(code)
}
},
isEnabled: state.selectedMatchCode == nil,
animateAlpha: false,
animateScale: true
)
@ -364,6 +462,7 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
)
context.add(emoji
.position(CGPoint(x: emojiOriginX + emojiSize.width / 2.0, y: contentHeight + emojiSize.height / 2.0))
.opacity(state.selectedMatchCode != nil && state.selectedMatchCode != code ? 0.6 : 1.0)
.appear(ComponentTransition.Appear({ _, view, transition in
if !transition.animation.isImmediate {
transition.animateAlpha(view: view, from: 0.0, to: 1.0, delay: emojiDelay)
@ -631,17 +730,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
guard let state else {
return
}
if state.displayEmoji {
state.displayEmoji = false
state.updated(transition: .spring(duration: 0.4))
} else {
let accountContext = state.forcedAccount?.0 ?? component.context
guard let accountPeer = state.forcedAccount?.1 ?? state.peer else {
return
}
component.completion(accountContext, accountPeer, .decline)
component.cancel(true)
let accountContext = state.forcedAccount?.0 ?? component.context
guard let accountPeer = state.forcedAccount?.1 ?? state.peer else {
return
}
component.completion(accountContext, accountPeer, .decline)
component.cancel(true)
}
),
availableSize: CGSize(width: buttonWidth, height: 52.0),
@ -669,12 +763,12 @@ private final class AuthConfirmationSheetContent: CombinedComponent {
guard let state else {
return
}
if let matchCodes, !matchCodes.isEmpty {
if !flags.contains(.showMatchCodesFirst), let matchCodes, !matchCodes.isEmpty {
state.displayEmoji = true
state.matchCodes = matchCodes.shuffled()
state.updated(transition: .spring(duration: 0.4))
} else {
complete(nil)
complete(state.selectedMatchCode)
}
}
),
@ -699,15 +793,18 @@ private final class AuthConfirmationSheetComponent: CombinedComponent {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let requestSubject: MessageActionUrlSubject
let subject: MessageActionUrlAuthResult
let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
init(
context: AccountContext,
requestSubject: MessageActionUrlSubject,
subject: MessageActionUrlAuthResult,
completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
) {
self.context = context
self.requestSubject = requestSubject
self.subject = subject
self.completion = completion
}
@ -731,6 +828,7 @@ private final class AuthConfirmationSheetComponent: CombinedComponent {
component: SheetComponent<EnvironmentType>(
content: AnyComponent<EnvironmentType>(AuthConfirmationSheetContent(
context: context.component.context,
requestSubject: context.component.requestSubject,
subject: context.component.subject,
completion: context.component.completion,
cancel: { animate in
@ -792,18 +890,22 @@ public class AuthConfirmationScreen: ViewControllerComponentContainer {
public enum Result {
case accept(allowWriteAccess: Bool, sharePhoneNumber: Bool, matchCode: String?)
case decline
case failed
}
private let context: AccountContext
private let requestSubject: MessageActionUrlSubject
private let subject: MessageActionUrlAuthResult
fileprivate let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
public init(
context: AccountContext,
requestSubject: MessageActionUrlSubject,
subject: MessageActionUrlAuthResult,
completion: @escaping (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void
) {
self.context = context
self.requestSubject = requestSubject
self.subject = subject
self.completion = completion
@ -811,6 +913,7 @@ public class AuthConfirmationScreen: ViewControllerComponentContainer {
context: context,
component: AuthConfirmationSheetComponent(
context: context,
requestSubject: requestSubject,
subject: subject,
completion: completion
),

View file

@ -1822,19 +1822,12 @@ func openResolvedUrlImpl(
|> deliverOnMainQueue).start(next: { result in
if case .request = result {
var dismissImpl: (() -> Void)?
let controller = AuthConfirmationScreen(context: context, subject: result, completion: { accountContext, accountPeer, authResult in
let controller = AuthConfirmationScreen(context: context, requestSubject: subject, subject: result, completion: { accountContext, accountPeer, authResult in
switch authResult {
case let .accept(allowWriteAccess, sharePhoneNumber, matchCode):
let signal: Signal<MessageActionUrlAuthResult, MessageActionUrlAuthError>
if accountContext === context {
signal = accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode)
} else {
accountContext.account.shouldBeServiceTaskMaster.set(.single(.now))
signal = accountContext.engine.messages.requestMessageActionUrlAuth(subject: subject)
|> castError(MessageActionUrlAuthError.self)
|> mapToSignal { result in
return accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode)
} |> afterDisposed {
let signal = accountContext.engine.messages.acceptMessageActionUrlAuth(subject: subject, allowWriteAccess: allowWriteAccess, sharePhoneNumber: sharePhoneNumber, matchCode: matchCode)
|> afterDisposed {
if accountContext !== context {
accountContext.account.shouldBeServiceTaskMaster.set(.single(.never))
}
}
@ -1882,8 +1875,9 @@ func openResolvedUrlImpl(
}
}
let isWebUrl = url.hasPrefix("http:") || url.hasPrefix("https:")
let openInOptions = availableOpenInOptions(context: context, item: .url(url: url))
if let match = openInOptions.first(where: { $0.identifier == browserIdentifier }), case let .openUrl(openUrl) = match.action() {
if isWebUrl, let match = openInOptions.first(where: { $0.identifier == browserIdentifier }), case let .openUrl(openUrl) = match.action() {
context.sharedContext.openExternalUrl(context: context, urlContext: .external, url: openUrl, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
} else {
context.sharedContext.openExternalUrl(context: context, urlContext: .external, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
@ -1901,6 +1895,15 @@ func openResolvedUrlImpl(
})
case .decline:
let _ = context.engine.messages.declineUrlAuth(url: url).start()
case .failed:
dismissImpl?()
if case let .request(domain, _, _, _, _, _) = result {
let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.AuthConfirmation_LoginFail_Title, text: presentationData.strings.AuthConfirmation_LoginFail_Text(domain).string, timeout: nil, customUndoText: nil), action: { _ in return true })
(navigationController?.topViewController as? ViewController)?.present(controller, in: .window(.root))
}
HapticFeedback().error()
}
})
navigationController?.pushViewController(controller)