From 654ff9e7e023b2e752e1e6639dc0ff0dc60008aa Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 24 Feb 2026 21:44:29 +0400 Subject: [PATCH 1/5] Various improvements --- .../BrowserUI/Sources/BrowserWebContent.swift | 15 +- .../Sources/OpenInOptions.swift | 4 +- submodules/TelegramApi/Sources/Api40.swift | 16 ++ .../RequestMessageActionCallback.swift | 53 +++--- .../Messages/TelegramEngineMessages.swift | 4 + .../Components/AuthConfirmationScreen/BUILD | 2 + .../Sources/AuthConfirmationScreen.swift | 155 +++++++++++++++--- .../TelegramUI/Sources/OpenResolvedUrl.swift | 27 +-- 8 files changed, 216 insertions(+), 60 deletions(-) diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 9d3f7de0ca..ee4c3c06b1 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -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 diff --git a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift index 98f02efe3a..7721f5bdb1 100644 --- a/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift +++ b/submodules/OpenInExternalAppUI/Sources/OpenInOptions.swift @@ -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) })) diff --git a/submodules/TelegramApi/Sources/Api40.swift b/submodules/TelegramApi/Sources/Api40.swift index 7dcb134207..a696afe4c2 100644 --- a/submodules/TelegramApi/Sources/Api40.swift +++ b/submodules/TelegramApi/Sources/Api40.swift @@ -5322,6 +5322,22 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func checkUrlAuthMatchCode(url: String, matchCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + 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) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift index ca0c925309..2a8adc463b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestMessageActionCallback.swift @@ -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 { + return account.network.request(Api.functions.messages.checkUrlAuthMatchCode(url: url, matchCode: matchCode)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> map { result in + return result == .boolTrue + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index fd987c94ae..2bfbbd482b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -106,6 +106,10 @@ public extension TelegramEngine { public func declineUrlAuth(url: String) -> Signal { return _internal_declineUrlAuth(account: self.account, url: url) } + + public func checkUrlAuthMatchCode(url: String, matchCode: String) -> Signal { + 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) diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD index 0dd57b9b18..4136ffb3e3 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/BUILD @@ -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", diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift index 8b97bdbdf8..6e10b9d8f9 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift @@ -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( content: AnyComponent(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 ), diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 412f7bbc79..9effcb12f9 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -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 - 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) From 9fe004a7ecad9a87e78c9cded03422382779f94b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 Feb 2026 02:08:33 +0400 Subject: [PATCH 2/5] Various improvements --- .../Sources/NotificationService.swift | 4 +++ .../Telegram-iOS/en.lproj/Localizable.strings | 1 + .../LegacyComponents/TGVideoEditAdjustments.h | 1 + .../Sources/TGVideoEditAdjustments.m | 5 ++++ .../Sources/LegacyAttachmentMenu.swift | 14 +++++++++- .../Sources/LegacyMediaPickers.swift | 16 ++++++++--- .../State/AccountStateManagementUtils.swift | 2 ++ .../Sources/AuthConfirmationScreen.swift | 24 ++++++++++++++-- .../TelegramUI/Sources/AppDelegate.swift | 18 ++++++++---- .../Sources/Chat/ChatControllerEditGif.swift | 12 +++++++- .../TelegramUI/Sources/ChatController.swift | 28 ++++++++++++++++--- .../TelegramUI/Sources/OpenResolvedUrl.swift | 23 +++++++++++---- 12 files changed, 123 insertions(+), 25 deletions(-) diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index b154af734f..e5be042e7f 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1223,6 +1223,10 @@ private final class NotificationServiceHandler { content.userInfo["peerId"] = "\(peerId.toInt64())" content.userInfo["accountId"] = "\(recordId.int64)" + if let dataUrl = payloadJson["data_url"] as? String { + content.userInfo["data_url"] = dataUrl + } + if let silentString = payloadJson["silent"] as? String { if let silentValue = Int(silentString), silentValue != 0 { content.silent = true diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 8f9b5cb365..f246c25197 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15752,6 +15752,7 @@ Error: %8$@"; "AuthConfirmation.LoginFail.Title" = "Login Failed"; "AuthConfirmation.LoginFail.Text" = "Please try logging in to [%@]() again."; +"AuthConfirmation.LoginFail.TextUnknown" = "Please try logging in 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/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h index c5925658a9..8c219596f8 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoEditAdjustments.h @@ -41,6 +41,7 @@ typedef enum - (bool)trimApplied; - (bool)isCropAndRotationEqualWith:(id)adjustments; +- (bool)isDefaultValuesForGif; - (NSDictionary *)dictionary; diff --git a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m index 88cfff3248..1a41c032c4 100644 --- a/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m +++ b/submodules/LegacyComponents/Sources/TGVideoEditAdjustments.m @@ -429,6 +429,11 @@ const NSTimeInterval TGVideoEditMaximumGifDuration = 30.5; return ![self cropAppliedForAvatar:forAvatar] && ![self toolsApplied] && ![self hasPainting] && !_sendAsGif && _preset == TGMediaVideoConversionPresetCompressedDefault; } +- (bool)isDefaultValuesForGif +{ + return ![self cropAppliedForAvatar:false] && ![self toolsApplied] && ![self hasPainting]; +} + - (bool)isCropEqualWith:(id)adjusments { return (_CGRectEqualToRectWithEpsilon(self.cropRect, adjusments.cropRect, [self _cropRectEpsilon])); diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index b062edc22a..acc28dacf4 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -157,7 +157,19 @@ public func legacyStoryMediaEditor(context: AccountContext, item: TGMediaEditabl }) } -public func legacyMediaEditor(context: AccountContext, peer: Peer, threadTitle: String?, media: AnyMediaReference, mode: LegacyMediaEditorMode, initialCaption: NSAttributedString, snapshots: [UIView], transitionCompletion: (() -> Void)?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, Bool) -> Void, present: @escaping (ViewController, Any?) -> Void) { +public func legacyMediaEditor( + context: AccountContext, + peer: Peer, + threadTitle: String?, + media: AnyMediaReference, + mode: LegacyMediaEditorMode, + initialCaption: NSAttributedString, + snapshots: [UIView], + transitionCompletion: (() -> Void)?, + getCaptionPanelView: @escaping () -> TGCaptionPanelView?, + sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, Bool) -> Void, + present: @escaping (ViewController, Any?) -> Void +) { let _ = (fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .other, mediaReference: media) |> deliverOnMainQueue).start(next: { (value, isImage) in guard case let .data(data) = value, data.complete else { diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 5b5b7d6be8..bb8addb1fa 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -374,7 +374,7 @@ public struct LegacyAssetPickerEnqueueMessage { public var isFile: Bool } -public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: Account, signals: [Any]) -> Signal<[LegacyAssetPickerEnqueueMessage], Void> { +public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: Account, signals: [Any], originalMediaReference: AnyMediaReference? = nil) -> Signal<[LegacyAssetPickerEnqueueMessage], Void> { return Signal { subscriber in let disposable = SSignal.combineSignals(signals).start(next: { anyValues in var messages: [LegacyAssetPickerEnqueueMessage] = [] @@ -934,8 +934,16 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A fileAttributes.append(.HasLinkedStickers) } - let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], videoCover: videoCover, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes, alternativeRepresentations: []) - + let media: Media + let mediaReference: AnyMediaReference + if let adjustments, adjustments.isDefaultValuesForGif(), let originalMediaReference { + media = originalMediaReference.media + mediaReference = originalMediaReference + } else { + media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: previewRepresentations, videoThumbnails: [], videoCover: videoCover, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: fileAttributes, alternativeRepresentations: []) + mediaReference = .standalone(media: media) + } + if let timer = item.timer, timer > 0 && (timer <= 60 || timer == viewOnceTimeout) { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: Int32(timer), countdownBeginTime: nil)) } @@ -987,7 +995,7 @@ public func legacyAssetPickerEnqueueMessages(context: AccountContext, account: A ) } } else { - messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile)) + messages.append(LegacyAssetPickerEnqueueMessage(message: .message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: mediaReference, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: item.groupedId, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets), uniqueId: item.uniqueId, isFile: asFile)) } } } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index cb67c334fc..bb12e5a493 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4329,6 +4329,8 @@ func replayFinalState( if !message.flags.contains(.Incoming), message.forwardInfo == nil { if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), let peer = transaction.getPeer(message.id.peerId), peer.isCopyProtectionEnabled { + } else if message.id.peerId.namespace == Namespaces.Peer.CloudUser, let cachedUserData = transaction.getPeerCachedData(peerId: message.id.peerId) as? CachedUserData, cachedUserData.flags.contains(.copyProtectionEnabled) || cachedUserData.flags.contains(.myCopyProtectionEnabled) { + } else { inner: for media in message.media { if let file = media as? TelegramMediaFile { diff --git a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift index 6e10b9d8f9..4efb10536e 100644 --- a/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift +++ b/submodules/TelegramUI/Components/AuthConfirmationScreen/Sources/AuthConfirmationScreen.swift @@ -64,6 +64,8 @@ private final class AuthConfirmationSheetContent: CombinedComponent { private let subject: MessageActionUrlAuthResult private let completion: (AccountContext, EnginePeer, AuthConfirmationScreen.Result) -> Void + private let disposables = DisposableSet() + var peer: EnginePeer? var forcedAccount: (AccountContext, EnginePeer)? @@ -92,9 +94,23 @@ private final class AuthConfirmationSheetContent: CombinedComponent { 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(_, _, _, flags, matchCodes, _) = self.subject, let matchCodes { + if flags.contains(.showMatchCodesFirst) { + self.displayEmoji = true + self.matchCodes = matchCodes.shuffled() + } else { + for code in matchCodes { + var file: TelegramMediaFile? + if let item = context.animatedEmojiStickersValue[code] { + file = item.first?.file._parse() + } else if let item = context.animatedEmojiStickersValue[code.strippedEmoji] { + file = item.first?.file._parse() + } + if let file { + self.disposables.add(freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: .standalone(media: file), resource: file.resource).start()) + } + } + } } if case let .request(_, _, _, _, _, userIdHint) = self.subject, let userIdHint, userIdHint != context.account.peerId { @@ -119,6 +135,8 @@ private final class AuthConfirmationSheetContent: CombinedComponent { } deinit { + self.disposables.dispose() + if !self.inProgress { if let (context, _) = self.forcedAccount { context.account.shouldBeServiceTaskMaster.set(.single(.never)) diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 068f80f757..6de065f2bc 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2811,13 +2811,19 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { accountId in if response.actionIdentifier == UNNotificationDefaultActionIdentifier { - if let (peerId, threadId) = peerIdFromNotification(response.notification) { - var messageId: MessageId? = nil - if response.notification.request.content.categoryIdentifier == "c" || response.notification.request.content.categoryIdentifier == "t" { - messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) + if let dataUrl = response.notification.request.content.userInfo["data_url"] as? String { + if let url = URL(string: dataUrl) { + self.openUrlWhenReady(url: url) + } + } else { + if let (peerId, threadId) = peerIdFromNotification(response.notification) { + var messageId: MessageId? = nil + if response.notification.request.content.categoryIdentifier == "c" || response.notification.request.content.categoryIdentifier == "t" { + messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) + } + let storyId = storyIdFromNotification(peerId: peerId, notification: response.notification) + self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: threadId, messageId: messageId, storyId: storyId) } - let storyId = storyIdFromNotification(peerId: peerId, notification: response.notification) - self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: threadId, messageId: messageId, storyId: storyId) } completionHandler() } else if response.actionIdentifier == "reply", let (peerId, threadId) = peerIdFromNotification(response.notification), let accountId = accountId { diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift index d179ee7688..dba3277cd6 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift @@ -31,7 +31,17 @@ extension ChatControllerImpl { guard let self else { return } - self.enqueueMediaMessages(fromGallery: false, signals: signals, silentPosting: false, scheduleTime: nil, replyToSubject: nil, parameters: nil, getAnimatedTransitionSource: nil, completion: {}) + self.enqueueMediaMessages( + fromGallery: false, + signals: signals, + originalMediaReference: file.abstract, + silentPosting: false, + scheduleTime: nil, + replyToSubject: nil, + parameters: nil, + getAnimatedTransitionSource: nil, + completion: {} + ) }, present: { [weak self] c, a in self?.present(c, in: .window(.root), with: a) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 37ef51110f..1b4f6d80d1 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8452,18 +8452,38 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } - func enqueueMediaMessages(fromGallery: Bool = false, signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, replyToSubject: ChatInterfaceState.ReplyMessageSubject? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { + func enqueueMediaMessages( + fromGallery: Bool = false, + signals: [Any]?, + originalMediaReference: AnyMediaReference? = nil, + silentPosting: Bool, + scheduleTime: Int32? = nil, + replyToSubject: ChatInterfaceState.ReplyMessageSubject? = nil, + parameters: ChatSendMessageActionSheetController.SendParameters? = nil, + getAnimatedTransitionSource: ((String) -> UIView?)? = nil, + completion: @escaping () -> Void = {} + ) { if let _ = self.presentationInterfaceState.sendPaidMessageStars { self.presentPaidMessageAlertIfNeeded(count: Int32(signals?.count ?? 1), forceDark: fromGallery, completion: { [weak self] postpone in self?.commitEnqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, postpone: postpone, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) }) } else { - self.commitEnqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime, replyToSubject: replyToSubject, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) + self.commitEnqueueMediaMessages(signals: signals, originalMediaReference: originalMediaReference, silentPosting: silentPosting, scheduleTime: scheduleTime, replyToSubject: replyToSubject, parameters: parameters, getAnimatedTransitionSource: getAnimatedTransitionSource, completion: completion) } } - private func commitEnqueueMediaMessages(signals: [Any]?, silentPosting: Bool, scheduleTime: Int32? = nil, postpone: Bool = false, replyToSubject: ChatInterfaceState.ReplyMessageSubject? = nil, parameters: ChatSendMessageActionSheetController.SendParameters? = nil, getAnimatedTransitionSource: ((String) -> UIView?)? = nil, completion: @escaping () -> Void = {}) { - self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!) + private func commitEnqueueMediaMessages( + signals: [Any]?, + originalMediaReference: AnyMediaReference? = nil, + silentPosting: Bool, + scheduleTime: Int32? = nil, + postpone: Bool = false, + replyToSubject: ChatInterfaceState.ReplyMessageSubject? = nil, + parameters: ChatSendMessageActionSheetController.SendParameters? = nil, + getAnimatedTransitionSource: ((String) -> UIView?)? = nil, + completion: @escaping () -> Void = {} + ) { + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(context: self.context, account: self.context.account, signals: signals!, originalMediaReference: originalMediaReference) |> deliverOnMainQueue).startStrict(next: { [weak self] items in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 9effcb12f9..7403a6a358 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1844,8 +1844,10 @@ func openResolvedUrlImpl( } 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)) + if let topViewController = navigationController?.topViewController as? ViewController { + let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AuthConfirmation_LoginSuccess_Title, text: text, cancel: nil, destructive: false), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) + topViewController.present(controller, in: .current) + } } } @@ -1887,8 +1889,10 @@ func openResolvedUrlImpl( 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)) + if let topViewController = navigationController?.topViewController as? ViewController { + 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), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) + topViewController.present(controller, in: .current) + } } HapticFeedback().error() @@ -1899,8 +1903,10 @@ func openResolvedUrlImpl( 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)) + if let topViewController = navigationController?.topViewController as? ViewController { + 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), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) + topViewController.present(controller, in: .current) + } } HapticFeedback().error() @@ -1910,6 +1916,11 @@ func openResolvedUrlImpl( dismissImpl = { [weak controller] in controller?.dismissAnimated() } + } else { + if let topViewController = navigationController?.topViewController as? ViewController { + let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.AuthConfirmation_LoginFail_Title, text: presentationData.strings.AuthConfirmation_LoginFail_TextUnknown, timeout: nil, customUndoText: nil), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) + topViewController.present(controller, in: .current) + } } }) } From d93a33cabb7d406c693298d18d481ca228d8fc02 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 Feb 2026 03:43:48 +0400 Subject: [PATCH 3/5] Update localization --- .../Telegram-iOS/en.lproj/Localizable.strings | 13 +++++ .../PeerCopyProtectionInfoScreen.swift | 17 ++++--- .../TelegramUI/Sources/OpenResolvedUrl.swift | 48 ++++++++++++------- .../Sources/SharedWakeupManager.swift | 12 ++--- 4 files changed, 59 insertions(+), 31 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f246c25197..b91bfe9217 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15928,7 +15928,20 @@ Error: %8$@"; "BackgroundTasks.UploadingStories_any" = "Uploading %d Stories"; "BackgroundTasks.StoryOpenAppToContinue" = "Open app to continue"; "BackgroundTasks.StorySubtitle" = "Running..."; +"BackgroundTasks.StoryFinished" = "Finished"; "BackgroundTasks.UploadingMedia_1" = "Sending Message"; "BackgroundTasks.UploadingMedia_any" = "Sending %d Messages"; "BackgroundTasks.MediaSubtitle" = "Running..."; +"BackgroundTasks.MediaFinished" = "Finished"; + +"DisableSharing.Title" = "Disable Sharing"; +"DisableSharing.Screenshot.Title" = "No Screenshots"; +"DisableSharing.Screenshot.Text" = "Disable screenshots and screen recordings in this chat."; + +"DisableSharing.Forwarding.Title" = "No Forwarding"; +"DisableSharing.Forwarding.Text" = "Disable message forwarding to other chats."; + +"DisableSharing.Saving.Title" = "No Saving"; +"DisableSharing.Saving.Text" = "Disable copying text and saving photos and videos to Photos."; +"DisableSharing.Confirm" = "Disable Sharing"; diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift index 96f6409cb2..ed1a58fd27 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerCopyProtectionInfoScreen/Sources/PeerCopyProtectionInfoScreen.swift @@ -100,7 +100,6 @@ private final class PeerCopyProtectionInfoSheetContent: CombinedComponent { let theme = environment.theme let strings = environment.strings - let _ = strings let sideInset: CGFloat = 30.0 + environment.safeInsets.left let textSideInset: CGFloat = 30.0 + environment.safeInsets.left @@ -131,7 +130,7 @@ private final class PeerCopyProtectionInfoSheetContent: CombinedComponent { let title = title.update( component: BalancedTextComponent( - text: .plain(NSAttributedString(string: "Disable Sharing", font: titleFont, textColor: textColor)), + text: .plain(NSAttributedString(string: strings.DisableSharing_Title, font: titleFont, textColor: textColor)), horizontalAlignment: .center, maximumNumberOfLines: 0, lineSpacing: 0.1 @@ -150,9 +149,9 @@ private final class PeerCopyProtectionInfoSheetContent: CombinedComponent { AnyComponentWithIdentity( id: "screenshot", component: AnyComponent(InfoParagraphComponent( - title: "No Screenshots", + title: strings.DisableSharing_Screenshot_Title, titleColor: textColor, - text: "Disable screenshots and screen recordings in this chat.", + text: strings.DisableSharing_Screenshot_Text, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/CopyProtection/NoScreenshot", @@ -164,9 +163,9 @@ private final class PeerCopyProtectionInfoSheetContent: CombinedComponent { AnyComponentWithIdentity( id: "forward", component: AnyComponent(InfoParagraphComponent( - title: "No Forwarding", + title: strings.DisableSharing_Forwarding_Title, titleColor: textColor, - text: "Disable message forwarding to other chats.", + text: strings.DisableSharing_Forwarding_Text, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/CopyProtection/NoForward", @@ -178,9 +177,9 @@ private final class PeerCopyProtectionInfoSheetContent: CombinedComponent { AnyComponentWithIdentity( id: "save", component: AnyComponent(InfoParagraphComponent( - title: "No Saving", + title: strings.DisableSharing_Saving_Title, titleColor: textColor, - text: "Disable copying text and saving photos and videos to Photos.", + text: strings.DisableSharing_Saving_Text, textColor: secondaryTextColor, accentColor: linkColor, iconName: "Premium/CopyProtection/NoDownload", @@ -227,7 +226,7 @@ private final class PeerCopyProtectionInfoSheetContent: CombinedComponent { if component.context.isPremium { buttonContent = AnyComponentWithIdentity(id: "disable", component: AnyComponent( ButtonTextContentComponent( - text: "Disable Sharing", + text: strings.DisableSharing_Confirm, badge: 0, textColor: theme.list.itemCheckColors.foregroundColor, badgeBackground: theme.list.itemCheckColors.foregroundColor, diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 7403a6a358..d6d50bdf75 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -1844,8 +1844,11 @@ func openResolvedUrlImpl( } else { text = presentationData.strings.AuthConfirmation_LoginSuccess_Text(domain).string } - if let topViewController = navigationController?.topViewController as? ViewController { - let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AuthConfirmation_LoginSuccess_Title, text: text, cancel: nil, destructive: false), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) + if var topViewController = navigationController?.topViewController as? ViewController { + if let tabBarController = topViewController as? TabBarController, let controller = tabBarController.currentController { + topViewController = controller + } + let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AuthConfirmation_LoginSuccess_Title, text: text, cancel: nil, destructive: false), action: { _ in return true }) topViewController.present(controller, in: .current) } } @@ -1888,28 +1891,38 @@ func openResolvedUrlImpl( }, error: { _ in dismissImpl?() - if case let .request(domain, _, _, _, _, _) = result { - if let topViewController = navigationController?.topViewController as? ViewController { - 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), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) - topViewController.present(controller, in: .current) + Queue.mainQueue().after(0.3) { + if case let .request(domain, _, _, _, _, _) = result { + if var topViewController = navigationController?.topViewController as? ViewController { + if let tabBarController = topViewController as? TabBarController, let controller = tabBarController.currentController { + topViewController = controller + } + 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 }) + topViewController.present(controller, in: .current) + } } + + HapticFeedback().error() } - - HapticFeedback().error() }) case .decline: let _ = context.engine.messages.declineUrlAuth(url: url).start() case .failed: dismissImpl?() - if case let .request(domain, _, _, _, _, _) = result { - if let topViewController = navigationController?.topViewController as? ViewController { - 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), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) - topViewController.present(controller, in: .current) + Queue.mainQueue().after(0.3) { + if case let .request(domain, _, _, _, _, _) = result { + if var topViewController = navigationController?.topViewController as? ViewController { + if let tabBarController = topViewController as? TabBarController, let controller = tabBarController.currentController { + topViewController = controller + } + 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 }) + topViewController.present(controller, in: .current) + } } + + HapticFeedback().error() } - - HapticFeedback().error() } }) navigationController?.pushViewController(controller) @@ -1917,8 +1930,11 @@ func openResolvedUrlImpl( controller?.dismissAnimated() } } else { - if let topViewController = navigationController?.topViewController as? ViewController { - let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.AuthConfirmation_LoginFail_Title, text: presentationData.strings.AuthConfirmation_LoginFail_TextUnknown, timeout: nil, customUndoText: nil), elevatedLayout: topViewController is TabBarController, action: { _ in return true }) + if var topViewController = navigationController?.topViewController as? ViewController { + if let tabBarController = topViewController as? TabBarController, let controller = tabBarController.currentController { + topViewController = controller + } + let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: presentationData.strings.AuthConfirmation_LoginFail_Title, text: presentationData.strings.AuthConfirmation_LoginFail_TextUnknown, timeout: nil, customUndoText: nil), action: { _ in return true }) topViewController.present(controller, in: .current) } } diff --git a/submodules/TelegramUI/Sources/SharedWakeupManager.swift b/submodules/TelegramUI/Sources/SharedWakeupManager.swift index dfb839298f..e259fd8693 100644 --- a/submodules/TelegramUI/Sources/SharedWakeupManager.swift +++ b/submodules/TelegramUI/Sources/SharedWakeupManager.swift @@ -514,7 +514,7 @@ public final class SharedWakeupManager { return } guard let self else { - task.updateTitle(task.title, subtitle: "Finished") + task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_MediaFinished) task.setTaskCompleted(success: true) return } @@ -562,7 +562,7 @@ public final class SharedWakeupManager { Task { @MainActor [weak self] in guard let self else { - task.updateTitle(task.title, subtitle: "Finished") + task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_MediaFinished) task.setTaskCompleted(success: true) return } @@ -576,7 +576,7 @@ public final class SharedWakeupManager { if self.backgroundProcessingTaskId != task.identifier || self.pendingMediaUploadsByKey.isEmpty { self.backgroundProcessingTaskProgressByKey = [:] - task.updateTitle(task.title, subtitle: "Finished") + task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_MediaFinished) task.setTaskCompleted(success: true) if self.backgroundProcessingTaskId == task.identifier { self.backgroundProcessingTaskId = nil @@ -690,7 +690,7 @@ public final class SharedWakeupManager { return } guard let self else { - task.updateTitle(task.title, subtitle: "Finished") + task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_StoryFinished) task.setTaskCompleted(success: true) return } @@ -738,7 +738,7 @@ public final class SharedWakeupManager { Task { @MainActor [weak self] in guard let self else { - task.updateTitle(task.title, subtitle: "Finished") + task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_StoryFinished) task.setTaskCompleted(success: true) return } @@ -754,7 +754,7 @@ public final class SharedWakeupManager { if self.backgroundStoryProcessingTaskId != task.identifier || self.pendingStoryUploadStatusesByKey.isEmpty { self.backgroundStoryProcessingTaskProgressByKey = [:] - task.updateTitle(task.title, subtitle: "Finished") + task.updateTitle(task.title, subtitle: presentationData.strings.BackgroundTasks_StoryFinished) task.setTaskCompleted(success: true) if self.backgroundStoryProcessingTaskId == task.identifier { self.backgroundStoryProcessingTaskId = nil From 7d65064f9b6678783e0881df5745dc37ad2c140a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 Feb 2026 04:38:32 +0400 Subject: [PATCH 4/5] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 +- .../Sources/ItemListAvatarAndNameItem.swift | 2 +- .../Sources/ChannelAdminController.swift | 16 +++++++++++++++- .../Sources/ChannelBannedMemberController.swift | 16 +++++++++++++++- .../Sources/ChatMessageDateHeader.swift | 2 +- .../Sources/RankChatPreviewItem.swift | 7 +++++-- 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index b91bfe9217..1c8d3932c6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -15910,7 +15910,7 @@ Error: %8$@"; "RankInfo.Owner.Title" = "Owner Tag"; "RankInfo.Owner.Text" = "This purple tag %1$@ is **%2$@'s** owner tag. **%3$@** is the owner of **%4$@**."; "RankInfo.ChangeInfo" = "Only admins can assign member tags in this group."; -"RankInfo.MemberTag" = "Admin Tag"; +"RankInfo.MemberTag" = "Member Tag"; "RankInfo.AdminTag" = "Admin Tag"; "RankInfo.OwnerTag" = "Owner Tag"; "RankInfo.SetMyTag" = "Set My Tag"; diff --git a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift index 7d415dc328..3c92955fe6 100644 --- a/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift +++ b/submodules/ItemListAvatarAndNameInfoItem/Sources/ItemListAvatarAndNameItem.swift @@ -194,7 +194,7 @@ public class ItemListAvatarAndNameInfoItem: ListViewItem, ItemListItem, ListItem if case .settings = mode { self.selectable = true } else { - self.selectable = false + self.selectable = action != nil } } diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index e1632e50f7..7e8eca0f3f 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -31,8 +31,9 @@ private final class ChannelAdminControllerArguments { let dismissInput: () -> Void let animateError: () -> Void let toggleIsOptionExpanded: (RightsItem.Sub) -> Void + let openPeer: () -> Void - init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void, toggleIsOptionExpanded: @escaping (RightsItem.Sub) -> Void) { + init(context: AccountContext, updateAdminRights: @escaping (Bool) -> Void, toggleRight: @escaping (RightsItem, TelegramChatAdminRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, transferOwnership: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissAdmin: @escaping () -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void, toggleIsOptionExpanded: @escaping (RightsItem.Sub) -> Void, openPeer: @escaping () -> Void) { self.context = context self.updateAdminRights = updateAdminRights self.toggleRight = toggleRight @@ -44,6 +45,7 @@ private final class ChannelAdminControllerArguments { self.dismissInput = dismissInput self.animateError = animateError self.toggleIsOptionExpanded = toggleIsOptionExpanded + self.openPeer = openPeer } } @@ -367,6 +369,8 @@ private enum ChannelAdminEntry: ItemListNodeEntry { case let .info(_, _, dateTimeFormat, peer, presence): return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, memberCount: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in }, avatarTapped: { + }, action: { + arguments.openPeer() }) case let .rankTitle(_, text, count, limit): var accessoryText: ItemListSectionHeaderAccessoryText? @@ -1219,6 +1223,16 @@ public func channelAdminController(context: AccountContext, updatedPresentationD return state } + }, openPeer: { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: adminId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + pushControllerImpl?(controller) + } + }) }) let presentationData = updatedPresentationData?.signal ?? context.sharedContext.presentationData diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 2a7f34d471..3c32e26178 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -22,14 +22,16 @@ private final class ChannelBannedMemberControllerArguments { let toggleIsOptionExpanded: (TelegramChatBannedRightsFlags) -> Void let openTimeout: () -> Void let delete: () -> Void + let openPeer: () -> Void - init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void) { + init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void, openPeer: @escaping () -> Void) { self.context = context self.toggleRight = toggleRight self.toggleRightWhileDisabled = toggleRightWhileDisabled self.toggleIsOptionExpanded = toggleIsOptionExpanded self.openTimeout = openTimeout self.delete = delete + self.openPeer = openPeer } } @@ -218,6 +220,8 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { case let .info(_, _, dateTimeFormat, peer, presence): return ItemListAvatarAndNameInfoItem(itemContext: .accountContext(arguments.context), presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, mode: .generic, peer: peer, presence: presence, memberCount: nil, state: ItemListAvatarAndNameInfoItemState(), sectionId: self.section, style: .blocks(withTopInset: true, withExtendedBottomInset: false), editingNameUpdated: { _ in }, avatarTapped: { + }, action: { + arguments.openPeer() }) case let .rightsHeader(_, text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) @@ -609,6 +613,16 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen }) ])]) presentControllerImpl?(actionSheet, nil) + }, openPeer: { + let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: memberId)) + |> deliverOnMainQueue).start(next: { peer in + guard let peer else { + return + } + if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { + pushControllerImpl?(controller) + } + }) }) var peerDataItems: [TelegramEngine.EngineData.Item.Peer.Peer] = [] diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift index f0babf06cc..42b4a3638f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageItemImpl/Sources/ChatMessageDateHeader.swift @@ -1054,7 +1054,7 @@ public final class ChatMessageAvatarHeaderNodeImpl: ListViewItemHeaderNode, Chat } public func updatePeer(peer: Peer) { - if let previousPeer = self.peer, previousPeer.nameColor != peer.nameColor || previousPeer.smallProfileImage != peer.smallProfileImage { + if let previousPeer = self.peer, previousPeer.nameColor != peer.nameColor || previousPeer.smallProfileImage != peer.smallProfileImage || previousPeer.displayLetters != peer.displayLetters { self.peer = peer if peer.smallProfileImage != nil { self.avatarNode.setPeerV2(context: self.context, theme: self.presentationData.theme.theme, peer: EnginePeer(peer), authorOfMessage: self.messageReference, overrideImage: nil, emptyColor: .black, synchronousLoad: false, displayDimensions: CGSize(width: avatarHeaderSize(), height: avatarHeaderSize())) diff --git a/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift b/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift index ee66c5d33b..4f917066be 100644 --- a/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift +++ b/submodules/TelegramUI/Components/RankChatPreviewItem/Sources/RankChatPreviewItem.swift @@ -232,11 +232,14 @@ final class RankChatPreviewItemNode: ListViewItemNode { var items: [ListViewItem] = [] for messageItem in item.messageItems.reversed() { - let authorPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)) + var userPeer = messageItem.peer._asPeer() + + let updatedId = PeerId.Id._internalFromInt64Value(userPeer.id.id._internalGetInt64Value() - 7) + let authorPeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: updatedId) let groupPeerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)) let groupPeer = TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, title: "", username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .member, info: .group(.init(flags: [])), flags: [], restrictionInfo: nil, adminRights: nil, bannedRights: nil, defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil) - var userPeer = messageItem.peer._asPeer() + if let user = userPeer as? TelegramUser { userPeer = TelegramUser(id: authorPeerId, accessHash: user.accessHash, firstName: user.firstName, lastName: user.lastName, username: "", phone: user.phone, photo: user.photo, botInfo: user.botInfo, restrictionInfo: user.restrictionInfo, flags: user.flags, emojiStatus: user.emojiStatus, usernames: user.usernames, storiesHidden: user.storiesHidden, nameColor: user.nameColor, backgroundEmojiId: user.backgroundEmojiId, profileColor: user.profileColor, profileBackgroundEmojiId: user.profileBackgroundEmojiId, subscriberCount: user.subscriberCount, verificationIconFileId: user.verificationIconFileId) } From c05a1dd17c5c07752bf84a1c5e44550ff60b95eb Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 25 Feb 2026 13:00:12 +0400 Subject: [PATCH 5/5] Various improvements --- .../Sources/ChannelAdminController.swift | 12 +- .../ChannelBannedMemberController.swift | 418 ++++++++++++++---- .../Sources/ChannelMembersController.swift | 12 +- .../PresentationResourcesRootController.swift | 6 +- .../ChatRecentActionsHistoryTransition.swift | 8 +- 5 files changed, 373 insertions(+), 83 deletions(-) diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index 7e8eca0f3f..e592965d63 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -1697,7 +1697,17 @@ public func channelAdminController(context: AccountContext, updatedPresentationD }) } - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let rightNavigationButton: ItemListNavigationButton? + if state.focusedOnRank { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightButtonActionImpl() + }) + footerItem = nil + } else { + rightNavigationButton = nil + } + + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelAdminControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelPeer: channelPeer, adminPeer: adminPeer, adminPresence: adminPresence, initialParticipant: initialParticipant, invite: invite, canEdit: canEdit), style: .blocks, focusItemTag: nil, ensureVisibleItemTag: nil, emptyStateItem: nil, footerItem: footerItem, animateChanges: true) diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 3c32e26178..fb83707153 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -15,6 +15,8 @@ import PresentationDataUtils import ItemListAvatarAndNameInfoItem import OldChannelsController +private let rankMaxLength: Int32 = 16 + private final class ChannelBannedMemberControllerArguments { let context: AccountContext let toggleRight: (TelegramChatBannedRightsFlags, Bool) -> Void @@ -23,8 +25,12 @@ private final class ChannelBannedMemberControllerArguments { let openTimeout: () -> Void let delete: () -> Void let openPeer: () -> Void + let updateRank: (String, String) -> Void + let updateFocusedOnRank: (Bool) -> Void + let dismissInput: () -> Void + let animateError: () -> Void - init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void, openPeer: @escaping () -> Void) { + init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, toggleRightWhileDisabled: @escaping (TelegramChatBannedRightsFlags) -> Void, toggleIsOptionExpanded: @escaping (TelegramChatBannedRightsFlags) -> Void, openTimeout: @escaping () -> Void, delete: @escaping () -> Void, openPeer: @escaping () -> Void, updateRank: @escaping (String, String) -> Void, updateFocusedOnRank: @escaping (Bool) -> Void, dismissInput: @escaping () -> Void, animateError: @escaping () -> Void) { self.context = context self.toggleRight = toggleRight self.toggleRightWhileDisabled = toggleRightWhileDisabled @@ -32,6 +38,10 @@ private final class ChannelBannedMemberControllerArguments { self.openTimeout = openTimeout self.delete = delete self.openPeer = openPeer + self.updateRank = updateRank + self.updateFocusedOnRank = updateFocusedOnRank + self.dismissInput = dismissInput + self.animateError = animateError } } @@ -40,6 +50,19 @@ private enum ChannelBannedMemberSection: Int32 { case rights case timeout case delete + case rank +} + +private enum ChannelBannedMemberEntryTag: ItemListItemTag { + case rank + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? ChannelBannedMemberEntryTag, self == other { + return true + } else { + return false + } + } } private enum ChannelBannedMemberEntryStableId: Hashable { @@ -49,6 +72,10 @@ private enum ChannelBannedMemberEntryStableId: Hashable { case timeout case exceptionInfo case delete + case rankTitle + case rankPreview + case rank + case rankInfo } private enum ChannelBannedMemberEntry: ItemListNodeEntry { @@ -58,6 +85,10 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { case timeout(PresentationTheme, String, String) case exceptionInfo(PresentationTheme, String) case delete(PresentationTheme, String) + case rankTitle(PresentationTheme, String, Int32?, Int32) + case rankPreview(PresentationTheme, PresentationStrings, EnginePeer, String, Bool) + case rank(PresentationTheme, PresentationStrings, String, String, Bool) + case rankInfo(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { @@ -69,6 +100,8 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { return ChannelBannedMemberSection.timeout.rawValue case .delete: return ChannelBannedMemberSection.delete.rawValue + case .rankTitle, .rankPreview, .rank, .rankInfo: + return ChannelBannedMemberSection.rank.rawValue } } @@ -86,6 +119,14 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { return .exceptionInfo case .delete: return .delete + case .rankTitle: + return .rankTitle + case .rankPreview: + return .rankPreview + case .rank: + return .rank + case .rankInfo: + return .rankInfo } } @@ -167,50 +208,109 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { } else { return false } + case let .rankTitle(lhsTheme, lhsText, lhsCount, lhsLimit): + if case let .rankTitle(rhsTheme, rhsText, rhsCount, rhsLimit) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsCount == rhsCount, lhsLimit == rhsLimit { + return true + } else { + return false + } + case let .rankPreview(lhsTheme, lhsStrings, lhsPeer, lhsRank, lhsIsOwner): + if case let .rankPreview(rhsTheme, rhsStrings, rhsPeer, rhsRank, rhsIsOwner) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPeer == rhsPeer, lhsRank == rhsRank, lhsIsOwner == rhsIsOwner { + return true + } else { + return false + } + case let .rank(lhsTheme, lhsStrings, lhsPlaceholder, lhsValue, lhsEnabled): + if case let .rank(rhsTheme, rhsStrings, rhsPlaceholder, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsValue == rhsValue, lhsEnabled == rhsEnabled { + return true + } else { + return false + } + case let .rankInfo(lhsTheme, lhsText, lhsTrimBottomInset): + if case let .rankInfo(rhsTheme, rhsText, rhsTrimBottomInset) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsTrimBottomInset == rhsTrimBottomInset { + return true + } else { + return false + } } } static func <(lhs: ChannelBannedMemberEntry, rhs: ChannelBannedMemberEntry) -> Bool { switch lhs { + case .info: + switch rhs { case .info: - switch rhs { - case .info: - return false - default: - return true - } - case .rightsHeader: - switch rhs { - case .info, .rightsHeader: - return false - default: - return true - } - case let .rightItem(_, lhsIndex, _, _, _, _, _, _): - switch rhs { - case .info, .rightsHeader: - return false - case let .rightItem(_, rhsIndex, _, _, _, _, _, _): - return lhsIndex < rhsIndex - default: - return true - } - case .timeout: - switch rhs { - case .delete, .exceptionInfo: - return true - default: - return false - } - case .exceptionInfo: - switch rhs { - case .delete: - return true - default: - return false - } - case .delete: return false + default: + return true + } + case .rightsHeader: + switch rhs { + case .info, .rightsHeader: + return false + default: + return true + } + case let .rightItem(_, lhsIndex, _, _, _, _, _, _): + switch rhs { + case .info, .rightsHeader: + return false + case let .rightItem(_, rhsIndex, _, _, _, _, _, _): + return lhsIndex < rhsIndex + default: + return true + } + case .timeout: + switch rhs { + case .info, .rightsHeader, .rightItem, .timeout: + return false + default: + return true + } + case .exceptionInfo: + switch rhs { + case .info, .rightsHeader, .rightItem, .timeout, .exceptionInfo: + return false + default: + return true + } + case .delete: + switch rhs { + case .info, .rightsHeader, .rightItem, .timeout, .exceptionInfo, .delete: + return false + default: + return true + } + case .rankTitle: + switch rhs { + case .info, .rightsHeader, .rightItem, .timeout, .delete, .rankTitle: + return false + default: + return true + } + case .rankPreview: + switch rhs { + case .info, .rightsHeader, .rightItem, .timeout, .delete, .rankTitle, .rankPreview: + return false + default: + return true + } + + case .rank: + switch rhs { + case .info, .rightsHeader, .rightItem, .timeout, .delete, .rankTitle, .rankPreview, .rank: + return false + default: + return true + } + + case .rankInfo: + switch rhs { + case .info, .rightsHeader, .rightItem, .timeout, .delete, .rankTitle, .rankPreview, .rank, .rankInfo: + return false + default: + return true + } } } @@ -269,6 +369,31 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: text, kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.delete() }) + case let .rankTitle(_, text, count, limit): + var accessoryText: ItemListSectionHeaderAccessoryText? + if let count = count { + accessoryText = ItemListSectionHeaderAccessoryText(value: "\(limit - count)", color: count > limit ? .destructive : .generic) + } + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: accessoryText, sectionId: self.section) + case let .rankPreview(_, _, peer, rank, _): + let globalPresentationData = arguments.context.sharedContext.currentPresentationData.with { $0 } + return arguments.context.sharedContext.makeChatRankPreviewItem(context: arguments.context, peer: peer, rank: rank, rankRole: .member, theme: presentationData.theme, strings: presentationData.strings, wallpaper: globalPresentationData.chatWallpaper, fontSize: globalPresentationData.chatFontSize, chatBubbleCorners: globalPresentationData.chatBubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, sectionId: self.section) + case let .rank(_, _, placeholder, text, enabled): + return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelBannedMemberEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in + arguments.updateRank(text, updatedText) + }, shouldUpdateText: { text in + if text.containsEmoji { + arguments.animateError() + return false + } + return true + }, updatedFocus: { focus in + arguments.updateFocusedOnRank(focus) + }, action: { + arguments.dismissInput() + }) + case let .rankInfo(_, text, trimBottomInset): + return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section, additionalOuterInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: trimBottomInset ? -44.0 : 0.0, right: 0.0)) } } } @@ -279,6 +404,8 @@ private struct ChannelBannedMemberControllerState: Equatable { var updatedTimeout: Int32? var updating: Bool = false var expandedPermissions = Set() + var updatedRank: String? + var focusedOnRank: Bool } func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { @@ -353,7 +480,22 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation index += 1 } - if !editMember { + if editMember { + let currentRank: String? + if let updatedRank = state.updatedRank { + currentRank = updatedRank + } else if let initialParticipant = initialParticipant { + currentRank = initialParticipant.rank + } else { + currentRank = nil + } + + let rankEnabled = !state.updating + entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) + entries.append(.rankPreview(presentationData.theme, presentationData.strings, member, currentRank ?? "0️⃣", false)) + entries.append(.rank(presentationData.theme, presentationData.strings, presentationData.strings.EditRank_Placeholder, currentRank ?? "", rankEnabled)) + entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagInfo(member.compactDisplayTitle).string, false)) + } else { entries.append(.timeout(presentationData.theme, presentationData.strings.GroupPermission_Duration, currentTimeoutString)) if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant, let initialBannedBy = initialBannedBy { @@ -415,7 +557,23 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation index += 1 } - if !editMember { + if editMember { + let currentRank: String? + if let updatedRank = state.updatedRank { + currentRank = updatedRank + } else if let initialParticipant = initialParticipant { + currentRank = initialParticipant.rank + } else { + currentRank = nil + } + + let rankEnabled = !state.updating + entries.append(.rankTitle(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagTitle.uppercased(), rankEnabled && state.focusedOnRank ? Int32(currentRank?.count ?? 0) : nil, rankMaxLength)) + entries.append(.rankPreview(presentationData.theme, presentationData.strings, member, currentRank ?? "0️⃣", false)) + entries.append(.rank(presentationData.theme, presentationData.strings, presentationData.strings.EditRank_Placeholder, currentRank ?? "", rankEnabled)) + entries.append(.rankInfo(presentationData.theme, presentationData.strings.Group_EditAdmin_MemberTagInfo(member.compactDisplayTitle).string, false)) + + } else { entries.append(.timeout(presentationData.theme, presentationData.strings.GroupPermission_Duration, currentTimeoutString)) if let initialParticipant = initialParticipant, case let .member(_, _, _, banInfo?, _, _) = initialParticipant, let initialBannedBy = initialBannedBy { @@ -429,7 +587,7 @@ private func channelBannedMemberControllerEntries(presentationData: Presentation } public func channelBannedMemberController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, memberId: PeerId, editMember: Bool = false, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChatBannedRights?) -> Void, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void) -> ViewController { - let initialState = ChannelBannedMemberControllerState(referenceTimestamp: Int32(Date().timeIntervalSince1970), updatedFlags: nil, updatedTimeout: nil, updating: false) + let initialState = ChannelBannedMemberControllerState(referenceTimestamp: Int32(Date().timeIntervalSince1970), updatedFlags: nil, updatedTimeout: nil, updating: false, updatedRank: nil, focusedOnRank: false) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((ChannelBannedMemberControllerState) -> ChannelBannedMemberControllerState) -> Void = { f in @@ -441,9 +599,15 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen let updateRightsDisposable = MetaDisposable() actionsDisposable.add(updateRightsDisposable) + let updateRankDisposable = MetaDisposable() + actionsDisposable.add(updateRankDisposable) + var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? + var dismissInputImpl: (() -> Void)? + var errorImpl: (() -> Void)? + var scrollToRankImpl: (() -> Void)? let peerView = Promise() peerView.set(context.account.viewTracker.peerView(peerId)) @@ -475,8 +639,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } else { effectiveRightsFlags = defaultBannedRightsFlags } - - + if rights == .banSendMedia { if value { effectiveRightsFlags.remove(rights) @@ -623,6 +786,28 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen pushControllerImpl?(controller) } }) + }, updateRank: { previousRank, updatedRank in + if updatedRank != previousRank { + updateState { state in + var state = state + state.updatedRank = updatedRank + return state + } + } + }, updateFocusedOnRank: { focusedOnRank in + updateState { state in + var state = state + state.focusedOnRank = focusedOnRank + return state + } + + if focusedOnRank { + scrollToRankImpl?() + } + }, dismissInput: { + dismissInputImpl?() + }, animateError: { + errorImpl?() }) var peerDataItems: [TelegramEngine.EngineData.Item.Peer.Peer] = [] @@ -670,6 +855,16 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen return } + var updateRank: String? + updateState { current in + updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) + return current + } + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + errorImpl?() + return + } + var resolvedRights: TelegramChatBannedRights? if let initialParticipant = initialParticipant { var updateFlags: TelegramChatBannedRightsFlags? @@ -738,12 +933,27 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen previousRights = banInfo?.rights } + let updateRankSignal: (PeerId) -> Signal + if let updateRank { + updateRankSignal = { peerId in + return context.peerChannelMemberCategoriesContextsManager.updateMemberRank(engine: context.engine, peerId: peerId, memberId: memberId, rank: updateRank) + |> `catch` { _ -> Signal in + return .single(Void()) + } + } + } else { + updateRankSignal = { _ in return .complete() } + } + if let resolvedRights = resolvedRights, previousRights != resolvedRights { let cleanResolvedRightsFlags = resolvedRights.flags.union(defaultBannedRightsFlags) let cleanResolvedRights = TelegramChatBannedRights(flags: cleanResolvedRightsFlags, untilDate: resolvedRights.untilDate) - + if cleanResolvedRights.flags.isEmpty && previousRights == nil { - dismissImpl?() + updateRankDisposable.set((updateRankSignal(peerId) + |> deliverOnMainQueue).start(completed: { + dismissImpl?() + })) } else { let applyRights: () -> Void = { updateState { state in @@ -770,9 +980,15 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen guard let upgradedPeerId = upgradedPeerId else { return .single(nil) } + + let rankSignal = updateRankSignal(upgradedPeerId) + return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: upgradedPeerId, memberId: memberId, bannedRights: cleanResolvedRights) |> mapToSignal { _ -> Signal in - return .complete() + return rankSignal + |> mapToSignal { _ -> Signal in + return .complete() + } } |> then(.single(upgradedPeerId)) } @@ -799,36 +1015,42 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen })) } else { updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(engine: context.engine, peerId: peerId, memberId: memberId, bannedRights: cleanResolvedRights) - |> deliverOnMainQueue).start(error: { _ in - - }, completed: { - if previousRights == nil { - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.GroupPermission_AddSuccess, false)), nil) - } - updated(cleanResolvedRights.flags.isEmpty ? nil : cleanResolvedRights) - dismissImpl?() - })) + |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + if previousRights == nil { + let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } + presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .genericSuccess(presentationData.strings.GroupPermission_AddSuccess, false)), nil) + } + updated(cleanResolvedRights.flags.isEmpty ? nil : cleanResolvedRights) + dismissImpl?() + })) + + updateRankDisposable.set(updateRankSignal(peerId).start()) } } - let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } - let actionSheet = ActionSheetController(presentationData: presentationData) - var items: [ActionSheetItem] = [] - items.append(ActionSheetTextItem(title: presentationData.strings.GroupPermission_ApplyAlertText(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string)) - items.append(ActionSheetButtonItem(title: presentationData.strings.GroupPermission_ApplyAlertAction, color: .accent, font: .default, enabled: true, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - applyRights() - })) - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, nil) + applyRights() +// let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } +// let actionSheet = ActionSheetController(presentationData: presentationData) +// var items: [ActionSheetItem] = [] +// items.append(ActionSheetTextItem(title: presentationData.strings.GroupPermission_ApplyAlertText(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string)) +// items.append(ActionSheetButtonItem(title: presentationData.strings.GroupPermission_ApplyAlertAction, color: .accent, font: .default, enabled: true, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// applyRights() +// })) +// actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ +// ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in +// actionSheet?.dismissAnimated() +// }) +// ])]) +// presentControllerImpl?(actionSheet, nil) } } else { - dismissImpl?() + updateRankDisposable.set((updateRankSignal(peerId) + |> deliverOnMainQueue).start(completed: { + dismissImpl?() + })) } }) } @@ -845,11 +1067,21 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen } } - let footerItem = ChannelParticipantFooterItem(theme: presentationData.theme, title: footerButtonTitle, displayProgress: state.updating, action: { - rightButtonActionImpl() - }) + let rightNavigationButton: ItemListNavigationButton? + let footerItem: ItemListControllerFooterItem? + if state.focusedOnRank { + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + rightButtonActionImpl() + }) + footerItem = nil + } else { + rightNavigationButton = nil + footerItem = ChannelParticipantFooterItem(theme: presentationData.theme, title: footerButtonTitle, displayProgress: state.updating, action: { + rightButtonActionImpl() + }) + } - let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: channelBannedMemberControllerEntries(presentationData: presentationData, state: state, accountPeerId: context.account.peerId, channelPeer: channelPeer, memberPeer: memberPeer, memberPresence: memberPresence, initialParticipant: initialParticipant, initialBannedBy: initialBannedByPeer, editMember: editMember), style: .blocks, emptyStateItem: nil, footerItem: footerItem, animateChanges: true) @@ -870,5 +1102,41 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen pushControllerImpl = { [weak controller] c in controller?.push(c) } + + let hapticFeedback = HapticFeedback() + errorImpl = { [weak controller] in + hapticFeedback.error() + controller?.forEachItemNode { itemNode in + if let itemNode = itemNode as? ItemListSingleLineInputItemNode { + itemNode.animateError() + } + } + } + scrollToRankImpl = { [weak controller] in + controller?.afterLayout({ + guard let controller = controller else { + return + } + + var resultItemNode: ListViewItemNode? + let _ = controller.frameForItemNode({ itemNode in + if let itemNode = itemNode as? ItemListSingleLineInputItemNode { + if let tag = itemNode.tag as? ChannelBannedMemberEntryTag, tag == .rank { + resultItemNode = itemNode + return true + } + } + return false + }) + if let resultItemNode = resultItemNode { + Queue.mainQueue().after(0.1) { + controller.ensureItemNodeVisible(resultItemNode, atTop: true) + } + } + }) + } + dismissInputImpl = { [weak controller] in + controller?.view.endEditing(true) + } return controller } diff --git a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift index 07afff8d60..036bdd4a28 100644 --- a/submodules/PeerInfoUI/Sources/ChannelMembersController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelMembersController.swift @@ -685,9 +685,15 @@ public func channelMembersController(context: AccountContext, updatedPresentatio })) }, openParticipant: { participant, isGroup in if isGroup { - let controller = channelBannedMemberController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, memberId: participant.peer.id, editMember: true, initialParticipant: participant.participant, updated: { rights in - }, upgradedToSupergroup: { _, _ in }) - pushControllerImpl?(controller) + if let _ = participant.participant.adminInfo { + let controller = channelAdminController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, adminId: participant.participant.peerId, initialParticipant: participant.participant, updated: { _ in + }, upgradedToSupergroup: { _, _ in }, transferedOwnership: { _ in }) + pushControllerImpl?(controller) + } else { + let controller = channelBannedMemberController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, memberId: participant.peer.id, editMember: true, initialParticipant: participant.participant, updated: { rights in + }, upgradedToSupergroup: { _, _ in }) + pushControllerImpl?(controller) + } } else { if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) { pushControllerImpl?(infoController) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift index 4ae03565ee..aa89cf5558 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesRootController.swift @@ -45,7 +45,7 @@ public struct PresentationResourcesRootController { public static func navigationShareIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.rootController.navigationBar.accentTextColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: theme.rootController.navigationBar.buttonColor) }) // return theme.image(PresentationResourceKey.navigationShareIcon.rawValue, generateShareButtonImage) } @@ -113,7 +113,7 @@ public struct PresentationResourcesRootController { public static func navigationMoreCircledIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationMoreCircledIcon.rawValue, { theme in - generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationMore"), color: theme.rootController.navigationBar.accentTextColor) + generateTintedImage(image: UIImage(bundleImageName: "Chat List/NavigationMore"), color: theme.rootController.navigationBar.buttonColor) }) } @@ -125,7 +125,7 @@ public struct PresentationResourcesRootController { public static func navigationAddIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.navigationAddIcon.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.accentTextColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat List/AddIcon"), color: theme.rootController.navigationBar.buttonColor) }) } diff --git a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift index 68a0cf2f86..ce0cecf39f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift +++ b/submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift @@ -2343,7 +2343,13 @@ struct ChatRecentActionsEntry: Comparable, Identifiable { return result }, to: &text, entities: &entities) } else { - + appendAttributedText(text: self.presentationData.strings.Channel_AdminLog_MessageRankNew(newRank), generateEntities: { index in + var result: [MessageTextEntityType] = [] + if index == 0 { + result.append(.Bold) + } + return result + }, to: &text, entities: &entities) } var attributes: [MessageAttribute] = [] if !entities.isEmpty {