From ea090a6858269642064994e41ae7665e1a4a5e4b Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Tue, 28 Apr 2026 23:53:26 +0200 Subject: [PATCH 1/5] Various improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 26 +++++++++- .../BrowserUI/Sources/BrowserMarkdown.swift | 8 +-- .../Sources/ChatListController.swift | 22 ++++++++ .../Sources/ChatTextLinkEditController.swift | 13 +++++ .../Sources/AdminUserActionsSheet.swift | 10 ++-- .../AlertMultilineInputFieldComponent.swift | 15 +++++- .../ChatMessagePollBubbleContentNode.swift | 51 +++++++++++-------- .../Sources/ChatScheduleTimeScreen.swift | 27 ++++------ .../Sources/ComposePollScreen.swift | 3 +- .../Sources/ContextActionsContainerNode.swift | 3 +- .../Sources/PeerInfoProfileItems.swift | 3 +- .../Sources/PeerInfoScreen.swift | 22 ++++---- .../Sources/PeerInfoSettingsItems.swift | 7 ++- .../QuickReplyNameAlertController.swift | 17 ++++++- .../TelegramUI/Sources/ChatController.swift | 11 +++- 15 files changed, 168 insertions(+), 70 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 41502d52e8..b7e3a23bc7 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16176,6 +16176,7 @@ Error: %8$@"; "CreatePoll.OptionsNeededOne" = "Add at least one option"; "CreatePoll.QuizCorrectOptionNeeded" = "Select a correct option"; "CreatePoll.QuizCorrectOptionNeededMultiple" = "Select at least one correct option"; +"CreatePoll.QuizCountryNeeded" = "Select at least one country"; "Stars.Intro.Transaction.Commission.Title" = "%@ commission"; @@ -16192,7 +16193,7 @@ Error: %8$@"; "CreatePoll.AllowedCountries.Countries_1" = "%@ country"; "CreatePoll.AllowedCountries.Countries_any" = "%@ countries"; -"Chat.Poll.Restriction.Subscribers" = "Only subscribers of **%@** can vote"; +"Chat.Poll.Restriction.Subscribers" = "Only subscribers of **%@** can vote."; "Chat.Poll.Restriction.Subscribers.TimeLimit" = "Only subscribers who joined more than **24 hours** ago can vote."; "Chat.Poll.Restriction.Country" = "Only users from %@ can vote."; "Chat.Poll.Restriction.SubscribersCountry" = "Only subscribers of **%@** from %@ can vote."; @@ -16232,3 +16233,26 @@ Error: %8$@"; "Login.Fee.GetPremiumForDays" = "Get Telegram Premium for %@"; "Login.Fee.GetPremiumForDays.Days_1" = "%@ day"; "Login.Fee.GetPremiumForDays.Days_any" = "%@ days"; + +"PeerInfo.DeleteReaction" = "Delete Reaction"; +"Chat.DeleteReactionInfo" = "Tap and hold to delete reaction."; + +"Chat.AdminActionSheet.DeleteReactionTitle" = "Delete 1 Reaction"; +"Chat.AdminActionSheet.DeleteAllMessages" = "Delete All Messages"; +"Chat.AdminActionSheet.DeleteAllReactions" = "Delete All Reactions"; + +"Conversation.CalendarSearch.Title" = "Search"; +"Conversation.CalendarSearch.Done" = "Done"; + +"ScheduleMessage.SilentPosting.YouEnabled" = "You will receive a silent notification"; +"ScheduleMessage.SilentPosting.YouDisabled" = "You will be notified"; +"ScheduleMessage.SilentPosting.UserEnabled" = "%@ will receive a silent notification"; +"ScheduleMessage.SilentPosting.UserDisabled" = "%@ will be notified"; +"ScheduleMessage.SilentPosting.GroupEnabled" = "Members will receive a silent notification"; +"ScheduleMessage.SilentPosting.GroupDisabled" = "Members will be notified"; +"ScheduleMessage.SilentPosting.ChannelEnabled" = "Subscribers will receive a silent notification"; +"ScheduleMessage.SilentPosting.ChannelDisabled" = "Subscribers will be notified"; + +"Settings.ChatAutomation" = "Chat Automation"; +"Settings.ChatAutomationInfo" = "Add a bot to reply to messages on your behalf."; +"Settings.ChatAutomationOff" = "Off"; diff --git a/submodules/BrowserUI/Sources/BrowserMarkdown.swift b/submodules/BrowserUI/Sources/BrowserMarkdown.swift index 6ef7a0eea5..c3723b4075 100644 --- a/submodules/BrowserUI/Sources/BrowserMarkdown.swift +++ b/submodules/BrowserUI/Sources/BrowserMarkdown.swift @@ -20,6 +20,7 @@ private let markdownInlineHTMLInlineIntent = InlinePresentationIntent(rawValue: private let markdownDefaultBlockImageDimensions = PixelDimensions(width: 1200, height: 900) private let markdownDefaultInlineImageDimensions = PixelDimensions(width: 18, height: 18) +private let markdownImageParsingEnabled = false private let markdownTaskListUncheckedNumber = "\u{001f}tg-md-task:unchecked" private let markdownTaskListCheckedNumber = "\u{001f}tg-md-task:checked" private let markdownRawHTMLTagRegex = try! NSRegularExpression(pattern: #"]*?>"#) @@ -353,6 +354,9 @@ private final class MarkdownConversionContext { } func resolveImage(attributes: [NSAttributedString.Key: Any]) -> MarkdownResolvedImage? { + guard markdownImageParsingEnabled else { + return nil + } guard let imageUrl = markdownImageURL(attributes: attributes) else { return nil } @@ -1166,10 +1170,8 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv switch level { case Int.min ... 1: return [.title(text)] - case 2: - return [.header(text)] default: - return [.heading(text: text, level: Int32(max(3, min(level, 6))))] + return [.heading(text: text, level: Int32(max(2, min(level, 6))))] } case .paragraph: guard let inlineContent = markdownInlineContent(from: node.attributedText, context: context) else { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 313f684120..c9c94d3288 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -7389,3 +7389,25 @@ private final class AdsInfoContextReferenceContentSource: ContextReferenceConten return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds.inset(by: self.insets), insets: self.contentInsets) } } + +public struct ChatListNavigationTarget { + public let chatListController: ChatListControllerImpl + public let popToController: ViewController? +} + +public func resolveChatListNavigationTarget(navigationController: NavigationController, excluding excludedController: ViewController? = nil) -> ChatListNavigationTarget? { + for case let controller as ViewController in navigationController.viewControllers.reversed() { + if let excludedController, controller === excludedController { + continue + } + if let chatListController = controller as? ChatListControllerImpl, !chatListController.previewing { + return ChatListNavigationTarget(chatListController: chatListController, popToController: controller) + } + } + + if let controller = navigationController.viewControllers.first as? TabBarController, let chatListController = controller.currentController as? ChatListControllerImpl { + return ChatListNavigationTarget(chatListController: chatListController, popToController: nil) + } + + return nil +} diff --git a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift index 4229f74605..87e357334b 100644 --- a/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift +++ b/submodules/ChatTextLinkEditUI/Sources/ChatTextLinkEditController.swift @@ -92,5 +92,18 @@ public func chatTextLinkEditController( dismissImpl = { [weak alertController] in alertController?.dismiss(completion: nil) } + + if link == nil { + Queue.mainQueue().after(0.1, { + let pasteboard = UIPasteboard.general + if pasteboard.hasURLs { + if inputState.value.string.isEmpty, let url = pasteboard.url?.absoluteString, !url.isEmpty { + let value = NSAttributedString(string: url) + inputState.setValue(value, selectionRange: 0 ..< value.length) + } + } + }) + } + return alertController } diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index b8bfed14be..2f7a4dc717 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -302,9 +302,8 @@ private func adminUserActionsTitle( } else { return strings.Chat_AdminActionSheet_DeleteTitle(Int32(messageCount)) } - case .chatReaction(_): - //TODO:localize - return "Delete 1 Reaction" + case .chatReaction: + return strings.Chat_AdminActionSheet_DeleteReactionTitle } } @@ -535,14 +534,13 @@ private final class AdminUserActionsContentComponent: Component { )))) } } else { - //TODO:localize subItems.append( AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: component.theme, style: .glass, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Delete all Messages", + string: component.strings.Chat_AdminActionSheet_DeleteAllMessages, font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), textColor: component.theme.list.itemPrimaryTextColor )), @@ -568,7 +566,7 @@ private final class AdminUserActionsContentComponent: Component { style: .glass, title: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: "Delete all Reactions", + string: component.strings.Chat_AdminActionSheet_DeleteAllReactions, font: Font.regular(component.presentationData.listsFontSize.baseDisplaySize), textColor: component.theme.list.itemPrimaryTextColor )), diff --git a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift index 49f9b1e7d7..1d484546b6 100644 --- a/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift +++ b/submodules/TelegramUI/Components/AlertComponent/AlertMultilineInputFieldComponent/Sources/AlertMultilineInputFieldComponent.swift @@ -19,6 +19,7 @@ public final class AlertMultilineInputFieldComponent: Component { public fileprivate(set) var value: NSAttributedString = NSAttributedString() public fileprivate(set) var animateError: () -> Void = {} public fileprivate(set) var activateInput: () -> Void = {} + fileprivate var setValueImpl: ((NSAttributedString, Range) -> Void)? fileprivate let valuePromise = ValuePromise(NSAttributedString()) public var valueSignal: Signal { return self.valuePromise.get() @@ -32,6 +33,13 @@ public final class AlertMultilineInputFieldComponent: Component { public init() { } + + public func setValue(_ value: NSAttributedString, selectionRange: Range? = nil) { + let selectionRange = selectionRange ?? (value.length ..< value.length) + self.value = value + self.valuePromise.set(value) + self.setValueImpl?(value, selectionRange) + } } public enum FormatMenuAvailability: Equatable { @@ -226,13 +234,18 @@ public final class AlertMultilineInputFieldComponent: Component { func update(component: AlertMultilineInputFieldComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { var resetText: NSAttributedString? if self.component == nil { - resetText = component.initialValue + resetText = component.initialValue ?? (component.externalState.value.length == 0 ? nil : component.externalState.value) component.externalState.animateError = { [weak self] in self?.animateError() } component.externalState.activateInput = { [weak self] in self?.activateInput() } + component.externalState.setValueImpl = { [weak self] value, selectionRange in + if let textFieldView = self?.textField.view as? TextFieldComponent.View { + textFieldView.updateText(value, selectionRange: selectionRange) + } + } } let isFirstTime = self.component == nil diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index d7cd526a60..6e87fb27b6 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -2918,31 +2918,42 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if case .public = poll.publicity { item.controllerInteraction.openMessagePollResults(item.message.id, option.opaqueIdentifier) } else if isRestricted { - let locale = localeWithStrings(item.presentationData.strings) - let countryNames = poll.countries.map { id in - if let countryName = locale.localizedString(forRegionCode: id) { - return countryName - } else { - return id - } - } - var countries: String = "" - if countryNames.count == 1, let country = countryNames.first { - countries = "**\(country)**" - } else { - for i in 0 ..< countryNames.count { - countries.append("**\(countryNames[i])**") - if i == countryNames.count - 2 { - countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter) - } else if i < countryNames.count - 2 { - countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter) + let text: String + + let peerName = item.message.peers[item.message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? "" + if !poll.countries.isEmpty { + let locale = localeWithStrings(item.presentationData.strings) + let countryNames = poll.countries.map { id in + if let countryName = locale.localizedString(forRegionCode: id) { + return countryName + } else { + return id } } + var countries: String = "" + if countryNames.count == 1, let country = countryNames.first { + countries = "**\(country)**" + } else { + for i in 0 ..< countryNames.count { + countries.append("**\(countryNames[i])**") + if i == countryNames.count - 2 { + countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesLastDelimiter) + } else if i < countryNames.count - 2 { + countries.append(item.presentationData.strings.Chat_Poll_Restriction_Country_CountriesDelimiter) + } + } + } + if poll.restrictToSubscribers { + text = item.presentationData.strings.Chat_Poll_Restriction_SubscribersCountry(peerName, countries).string + } else { + text = item.presentationData.strings.Chat_Poll_Restriction_Country(countries).string + } + } else { + text = item.presentationData.strings.Chat_Poll_Restriction_Subscribers(peerName).string } - //TODO:localize let controller = UndoOverlayController( presentationData: item.context.sharedContext.currentPresentationData.with { $0 }, - content: .banned(text: "Only users from \(countries) can vote."), + content: .banned(text: text), elevatedLayout: true, position: .bottom, action: { _ in return true } diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index d36aec58d2..58ec1e213a 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -174,42 +174,39 @@ private final class ChatScheduleTimeSheetContentComponent: Component { let isSilentPosting = self.isSilentPosting let _ = (component.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self, let peer, let controller = self.environment?.controller() else { + guard let self, let peer, let environment = self.environment, let controller = self.environment?.controller() else { return } - var isChannel = false if case let .channel(channel) = peer, case .broadcast = channel.info { isChannel = true } - - //TODO:localize let text: String if case let .user(user) = peer { if user.id == component.context.account.peerId { if isSilentPosting { - text = "You will receive a silent notification" + text = environment.strings.ScheduleMessage_SilentPosting_YouEnabled } else { - text = "You will be notified" + text = environment.strings.ScheduleMessage_SilentPosting_YouDisabled } } else { if isSilentPosting { - text = "\(peer.compactDisplayTitle) will receive a silent notification" + text = environment.strings.ScheduleMessage_SilentPosting_UserEnabled(peer.compactDisplayTitle).string } else { - text = "\(peer.compactDisplayTitle) will be notified" + text = environment.strings.ScheduleMessage_SilentPosting_UserDisabled(peer.compactDisplayTitle).string } } } else if isChannel { if isSilentPosting { - text = "Subscribers will receive a silent notification" + text = environment.strings.ScheduleMessage_SilentPosting_ChannelEnabled } else { - text = "Subscribers will be notified" + text = environment.strings.ScheduleMessage_SilentPosting_ChannelDisabled } } else { if isSilentPosting { - text = "Members will receive a silent notification" + text = environment.strings.ScheduleMessage_SilentPosting_GroupEnabled } else { - text = "Members will be notified" + text = environment.strings.ScheduleMessage_SilentPosting_GroupDisabled } } @@ -272,8 +269,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { case .poll: title = strings.CreatePoll_Deadline_Title case .search: - //TODO:localize - title = "Search" + title = strings.Conversation_CalendarSearch_Title } let titleSize = self.title.update( transition: transition, @@ -554,8 +550,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { case .poll: buttonTitle = strings.CreatePoll_Deadline_SetDeadline case .search: - //TODO:localize - buttonTitle = "Done" + buttonTitle = strings.Conversation_CalendarSearch_Done } let buttonSideInset: CGFloat = 30.0 diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index 42855d33c5..d365dfc98a 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -3099,9 +3099,8 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont text = presentationData.strings.CreatePoll_QuizCorrectOptionNeeded } case .countriesNeeded: - //TODO:localize title = nil - text = "Select at least one country" + text = presentationData.strings.CreatePoll_QuizCountryNeeded } let controller = UndoOverlayController( diff --git a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift index 09b4cb1251..40aa79c5bf 100644 --- a/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift +++ b/submodules/TelegramUI/Components/ContextControllerImpl/Sources/ContextActionsContainerNode.swift @@ -426,8 +426,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode { icon = UIImage(bundleImageName: "Chat/Context Menu/Tip") case .deleteReaction: self.action = nil - //TODO:localize - self.text = "Tap and hold to delete reaction." + self.text = self.presentationData.strings.Chat_DeleteReactionInfo self.targetSelectionIndex = nil icon = nil isUserInteractionEnabled = false diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift index 799802ed35..be77e08247 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift @@ -378,8 +378,7 @@ func infoItems( if let reactionSourceMessageId = reactionSourceMessageId { if canDeleteReaction { - //TODO:localize - items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemDeleteReaction, text: "Delete Reaction", color: .destructive, action: { + items[currentPeerInfoSection]!.append(PeerInfoScreenActionItem(id: ItemDeleteReaction, text: presentationData.strings.PeerInfo_DeleteReaction, color: .destructive, action: { interaction.openDeleteReaction(reactionSourceMessageId) })) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index d388044837..2e441623ba 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -4592,20 +4592,20 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro guard let controller = self.controller, let navigationController = controller.navigationController as? NavigationController else { return } - guard let tabController = navigationController.viewControllers.first as? TabBarController else { + guard let navigationTarget = resolveChatListNavigationTarget(navigationController: navigationController, excluding: controller) else { return } - for childController in tabController.controllers { - if let chatListController = childController as? ChatListController { - chatListController.maybeAskForPeerChatRemoval(peer: EngineRenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in - if deleted { - navigationController?.popToRoot(animated: true) - } - }, removed: { - }) - break + navigationTarget.chatListController.maybeAskForPeerChatRemoval(peer: EngineRenderedPeer(peer: peer), joined: false, deleteGloballyIfPossible: globally, completion: { [weak navigationController] deleted in + guard deleted, let navigationController = navigationController else { + return } - } + if let popToController = navigationTarget.popToController { + let _ = navigationController.popToViewController(popToController, animated: true) + } else { + navigationController.popToRoot(animated: true) + } + }, removed: { + }) } func deleteProfilePhoto(_ item: PeerInfoAvatarListItem) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift index 3bc0da8950..32aa2f6619 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift @@ -496,18 +496,17 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte } } - //TODO:localize let automationBotTitle: String if let botPeer = data.businessConnectedBot { let _ = botPeer automationBotTitle = "@\(botPeer.compactDisplayTitle)" } else { - automationBotTitle = "Off" + automationBotTitle = presentationData.strings.Settings_ChatAutomationOff } - items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerChatAutomation, label: .text(automationBotTitle), additionalBadgeLabel: nil, text: "Chat Automation", icon: PresentationResourcesSettings.aiTools, action: { + items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPeerChatAutomation, label: .text(automationBotTitle), additionalBadgeLabel: nil, text: presentationData.strings.Settings_ChatAutomation, icon: PresentationResourcesSettings.aiTools, action: { interaction.editingOpenBusinessChatBots() })) - items[.info]!.append(PeerInfoScreenCommentItem(id: ItemPeerChatAutomationHelp, text: "Add a bot to reply to messages on your behalf.")) + items[.info]!.append(PeerInfoScreenCommentItem(id: ItemPeerChatAutomationHelp, text: presentationData.strings.Settings_ChatAutomationInfo)) items[.account]!.append(PeerInfoScreenActionItem(id: ItemAddAccount, text: presentationData.strings.Settings_AddAnotherAccount, alignment: .center, action: { interaction.openSettings(.addAccount) diff --git a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift index f44510235a..466a1b8bd8 100644 --- a/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift +++ b/submodules/TelegramUI/Components/Settings/QuickReplyNameAlertController/Sources/QuickReplyNameAlertController.swift @@ -49,7 +49,7 @@ public func quickReplyNameAlertController(context: AccountContext, updatedPresen component: AnyComponent( AlertInputFieldComponent( context: context, - initialValue: nil, + initialValue: value, placeholder: strings.QuickReply_ShortcutPlaceholder, characterLimit: characterLimit, hasClearButton: false, @@ -58,6 +58,21 @@ public func quickReplyNameAlertController(context: AccountContext, updatedPresen autocorrectionType: .no, isInitiallyFocused: true, externalState: inputState, + shouldChangeText: { updatedText in + if updatedText.isEmpty { + return true + } + for scalar in updatedText.unicodeScalars { + if scalar.value == 0x5f || scalar.value == 0x200c || scalar.value == 0xb7 || (scalar.value >= 0xd80 && scalar.value <= 0xdff) { + continue + } + if CharacterSet.letters.contains(scalar) || CharacterSet.decimalDigits.contains(scalar) { + continue + } + return false + } + return true + }, returnKeyAction: { applyImpl?() } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 521ea3d5c1..fa5f539165 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -9385,10 +9385,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard case let .peer(peerId) = self.chatLocation else { return } + let navigationTarget = self.effectiveNavigationController.flatMap { navigationController in + resolveChatListNavigationTarget(navigationController: navigationController, excluding: self) + } self.commitPurposefulAction() self.chatDisplayNode.historyNode.disconnect() let _ = self.context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: reportChatSpam).startStandalone() - self.effectiveNavigationController?.popToRoot(animated: true) + if let navigationController = self.effectiveNavigationController { + if let navigationTarget, let popToController = navigationTarget.popToController { + let _ = navigationController.popToViewController(popToController, animated: true) + } else { + navigationController.popToRoot(animated: true) + } + } let _ = self.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peerId, isBlocked: true).startStandalone() } From 7ab4685940f2f20252524d8f6225192b7295be60 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 29 Apr 2026 03:52:19 +0200 Subject: [PATCH 2/5] Various fixes --- submodules/Camera/Sources/Camera.swift | 9 +- submodules/Camera/Sources/CameraDevice.swift | 24 ++-- submodules/Camera/Sources/CameraOutput.swift | 111 +++++++++++++----- .../Camera/Sources/PhotoCaptureContext.swift | 1 + submodules/Camera/Sources/VideoRecorder.swift | 4 +- .../Sources/TGPhotoCaptionInputMixin.m | 10 ++ .../ProxyListSettingsController.swift | 6 +- .../ProxyServerSettingsController.swift | 8 +- .../Sources/CameraVideoSource.swift | 4 +- .../Sources/ChatTextInputPanelNode.swift | 27 +++-- 10 files changed, 140 insertions(+), 64 deletions(-) diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 6790ba59ec..d879212c91 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -146,7 +146,7 @@ private final class CameraContext { transform = CGAffineTransformTranslate(transform, 0.0, -size.height) ciImage = ciImage.transformed(by: transform) } - ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 100.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) + ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: Camera.isDualCameraSupported(forRoundVideo: true) ? 60.0 : 40.0).cropped(to: CGRect(origin: .zero, size: size)) if let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) if front { @@ -189,6 +189,7 @@ private final class CameraContext { deinit { Logger.shared.log("CameraContext", "deinit") + NotificationCenter.default.removeObserver(self) } private var isSessionRunning = false @@ -202,7 +203,7 @@ private final class CameraContext { } func stopCapture(invalidate: Bool = false) { - Logger.shared.log("CameraContext", "startCapture(invalidate: \(invalidate))") + Logger.shared.log("CameraContext", "stopCapture(invalidate: \(invalidate))") if invalidate { self.mainDeviceContext?.device.resetZoom() @@ -212,6 +213,7 @@ private final class CameraContext { } self.session.session.stopRunning() + self.isSessionRunning = false } func focus(at point: CGPoint, autoFocus: Bool) { @@ -228,7 +230,7 @@ private final class CameraContext { } func setFps(_ fps: Float64) { - self.mainDeviceContext?.device.fps = fps + self.mainDeviceContext?.device.setFps(fps) } private var modeChange: Camera.ModeChange = .none { @@ -1170,6 +1172,7 @@ public struct CameraRecordingData { } public enum CameraRecordingError { + case videoRecorderInitializationError case audioInitializationError } diff --git a/submodules/Camera/Sources/CameraDevice.swift b/submodules/Camera/Sources/CameraDevice.swift index 6778c2dff5..e07d634d41 100644 --- a/submodules/Camera/Sources/CameraDevice.swift +++ b/submodules/Camera/Sources/CameraDevice.swift @@ -141,10 +141,12 @@ final class CameraDevice { Logger.shared.log("Camera", "No format selected") } + #if DEBUG Logger.shared.log("Camera", "Available formats:") for format in device.formats { Logger.shared.log("Camera", format.description) } + #endif if let targetFPS = device.actualFPS(maxFramerate) { device.activeVideoMinFrameDuration = targetFPS.duration @@ -180,18 +182,16 @@ final class CameraDevice { self.setFocusPoint(CGPoint(x: 0.5, y: 0.5), focusMode: .continuousAutoFocus, exposureMode: .continuousAutoExposure, monitorSubjectAreaChange: false) } - var fps: Double = defaultFPS { - didSet { - guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else { - return - } - - self.fps = targetFPS.fps - - self.transaction(device) { device in - device.activeVideoMinFrameDuration = targetFPS.duration - device.activeVideoMaxFrameDuration = targetFPS.duration - } + private(set) var fps: Double = defaultFPS + + func setFps(_ fps: Double) { + guard let device = self.videoDevice, let targetFPS = device.actualFPS(Double(self.fps)) else { + return + } + self.fps = targetFPS.fps + self.transaction(device) { device in + device.activeVideoMinFrameDuration = targetFPS.duration + device.activeVideoMaxFrameDuration = targetFPS.duration } } diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index a1f0a3fb1c..4872e7635b 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -93,6 +93,11 @@ public struct CameraCode: Equatable { } final class CameraOutput: NSObject { + private struct RoundVideoFormatDescriptionCacheEntry { + let sourceFormatDescription: CMFormatDescription + let outputFormatDescription: CMFormatDescription + } + let exclusive: Bool let ciContext: CIContext let colorSpace: CGColorSpace @@ -111,13 +116,14 @@ final class CameraOutput: NSObject { private var roundVideoFilter: CameraRoundLegacyVideoFilter? private let semaphore = DispatchSemaphore(value: 1) + private var roundVideoFormatDescriptionCache: [RoundVideoFormatDescriptionCacheEntry] = [] private let videoQueue = DispatchQueue(label: "", qos: .userInitiated) private let audioQueue = DispatchQueue(label: "") private let metadataQueue = DispatchQueue(label: "") - private var photoCaptureRequests: [Int64: PhotoCaptureContext] = [:] + private var photoCaptureRequests = Atomic<[Int64: PhotoCaptureContext]>(value: [:]) private var videoRecorder: VideoRecorder? private var captureOrientation: AVCaptureVideoOrientation = .portrait @@ -268,8 +274,8 @@ final class CameraOutput: NSObject { return EmptyDisposable } subscriber.putNext(self.photoOutput.isFlashScene) - let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { device, _ in - subscriber.putNext(self.photoOutput.isFlashScene) + let observer = self.photoOutput.observe(\.isFlashScene, options: [.new], changeHandler: { output, _ in + subscriber.putNext(output.isFlashScene) }) return ActionDisposable { observer.invalidate() @@ -316,12 +322,20 @@ final class CameraOutput: NSObject { #else let uniqueId = settings.uniqueID let photoCapture = PhotoCaptureContext(ciContext: self.ciContext, settings: settings, orientation: orientation, mirror: mirror) - self.photoCaptureRequests[uniqueId] = photoCapture + let _ = self.photoCaptureRequests.modify { dict in + var dict = dict + dict[uniqueId] = photoCapture + return dict + } self.photoOutput.capturePhoto(with: settings, delegate: photoCapture) return photoCapture.signal |> afterDisposed { [weak self] in - self?.photoCaptureRequests.removeValue(forKey: uniqueId) + let _ = self?.photoCaptureRequests.modify { dict in + var dict = dict + dict.removeValue(forKey: uniqueId) + return dict + } } #endif } @@ -419,18 +433,21 @@ final class CameraOutput: NSObject { } } ) + guard let videoRecorder else { + return .fail(.videoRecorderInitializationError) + } - videoRecorder?.start() + videoRecorder.start() self.videoRecorder = videoRecorder if case .dualCamera = mode, let position { - videoRecorder?.markPositionChange(position: position, time: .zero) + videoRecorder.markPositionChange(position: position, time: .zero) } else if case .roundVideo = mode { additionalOutput?.masterOutput = self } return Signal { subscriber in - let timer = SwiftSignalKit.Timer(timeout: 0.033, repeat: true, completion: { [weak videoRecorder] in + let timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { [weak videoRecorder] in let recordingData = CameraRecordingData(duration: videoRecorder?.duration ?? 0.0, filePath: outputFilePath) subscriber.putNext(recordingData) }, queue: Queue.mainQueue()) @@ -456,13 +473,54 @@ final class CameraOutput: NSObject { } var transitionImage: UIImage? { - return self.videoRecorder?.transitionImage + var result: UIImage? + self.videoQueue.sync { + result = self.videoRecorder?.transitionImage + } + return result } private weak var masterOutput: CameraOutput? private var lastSampleTimestamp: CMTime? + private func roundVideoFormatDescription(for sourceFormatDescription: CMFormatDescription) -> CMFormatDescription? { + if let entry = self.roundVideoFormatDescriptionCache.first(where: { CFEqual($0.sourceFormatDescription, sourceFormatDescription) }) { + return entry.outputFormatDescription + } + + guard let extensions = CMFormatDescriptionGetExtensions(sourceFormatDescription) as? [String: Any] else { + return nil + } + + let mediaSubType = CMFormatDescriptionGetMediaSubType(sourceFormatDescription) + var updatedExtensions = extensions + updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4 + + var outputFormatDescription: CMFormatDescription? + let status = CMVideoFormatDescriptionCreate( + allocator: nil, + codecType: mediaSubType, + width: videoMessageDimensions.width, + height: videoMessageDimensions.height, + extensions: updatedExtensions as CFDictionary, + formatDescriptionOut: &outputFormatDescription + ) + guard status == noErr, let outputFormatDescription else { + return nil + } + + self.roundVideoFormatDescriptionCache.append(RoundVideoFormatDescriptionCacheEntry( + sourceFormatDescription: sourceFormatDescription, + outputFormatDescription: outputFormatDescription + )) + if self.roundVideoFormatDescriptionCache.count > 4 { + self.roundVideoFormatDescriptionCache.removeFirst(self.roundVideoFormatDescriptionCache.count - 4) + } + + return outputFormatDescription + } + private var needsCrossfadeTransition = false private var crossfadeTransitionStart: Double = 0.0 @@ -564,17 +622,11 @@ final class CameraOutput: NSObject { return nil } self.semaphore.wait() - - let mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription) - let extensions = CMFormatDescriptionGetExtensions(formatDescription) as! [String: Any] - - var updatedExtensions = extensions - updatedExtensions["CVBytesPerRow"] = videoMessageDimensions.width * 4 - - var newFormatDescription: CMFormatDescription? - var status = CMVideoFormatDescriptionCreate(allocator: nil, codecType: mediaSubType, width: videoMessageDimensions.width, height: videoMessageDimensions.height, extensions: updatedExtensions as CFDictionary, formatDescriptionOut: &newFormatDescription) - guard status == noErr, let newFormatDescription else { + defer { self.semaphore.signal() + } + + guard let newFormatDescription = self.roundVideoFormatDescription(for: formatDescription) else { return nil } @@ -585,12 +637,11 @@ final class CameraOutput: NSObject { filter = CameraRoundLegacyVideoFilter(ciContext: self.ciContext, colorSpace: self.colorSpace, simple: self.exclusive) self.roundVideoFilter = filter } - if !filter.isPrepared { + if !filter.isPrepared || filter.inputFormatDescription.map({ !CFEqual($0, newFormatDescription) }) ?? true { filter.prepare(with: newFormatDescription, outputRetainedBufferCountHint: 4) } guard let newPixelBuffer = filter.render(pixelBuffer: videoPixelBuffer, additional: additional, captureOrientation: self.captureOrientation, transitionFactor: transitionFactor) else { - self.semaphore.signal() return nil } @@ -603,7 +654,7 @@ final class CameraOutput: NSObject { } var newSampleBuffer: CMSampleBuffer? - status = CMSampleBufferCreateForImageBuffer( + let status = CMSampleBufferCreateForImageBuffer( allocator: kCFAllocatorDefault, imageBuffer: newPixelBuffer, dataReady: true, @@ -615,10 +666,8 @@ final class CameraOutput: NSObject { ) if status == noErr, let newSampleBuffer { - self.semaphore.signal() return newSampleBuffer } - self.semaphore.signal() return nil } @@ -640,18 +689,18 @@ extension CameraOutput: AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureA guard CMSampleBufferDataIsReady(sampleBuffer) else { return } - - if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { - self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) - } else if sampleBuffer.type == kCMMediaType_Audio { - self.processAudioBuffer?(sampleBuffer) - } - + if let masterOutput = self.masterOutput { masterOutput.processVideoRecording(sampleBuffer, fromAdditionalOutput: true) } else { self.processVideoRecording(sampleBuffer, fromAdditionalOutput: false) } + + if let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { + self.processSampleBuffer?(sampleBuffer, videoPixelBuffer, connection) + } else if sampleBuffer.type == kCMMediaType_Audio { + self.processAudioBuffer?(sampleBuffer) + } } func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { diff --git a/submodules/Camera/Sources/PhotoCaptureContext.swift b/submodules/Camera/Sources/PhotoCaptureContext.swift index c61fe73602..3e34fc5904 100644 --- a/submodules/Camera/Sources/PhotoCaptureContext.swift +++ b/submodules/Camera/Sources/PhotoCaptureContext.swift @@ -56,6 +56,7 @@ final class PhotoCaptureContext: NSObject, AVCapturePhotoCaptureDelegate { } else { guard let photoPixelBuffer = photo.pixelBuffer else { print("Error occurred while capturing photo: Missing pixel buffer (\(String(describing: error)))") + self.pipe.putNext(.failed) return } diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index a2d3a7cb83..5748938a75 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -204,9 +204,7 @@ private final class VideoRecorderImpl { let maxDate = Date(timeIntervalSinceNow: 0.05) RunLoop.current.run(until: maxDate) } - } - if let videoInput = self.videoInput { let time = CACurrentMediaTime() // if let previousPresentationTime = self.previousPresentationTime, let previousAppendTime = self.previousAppendTime { // print("appending \(presentationTime.seconds) (\(presentationTime.seconds - previousPresentationTime) ) on \(time) (\(time - previousAppendTime)") @@ -366,7 +364,7 @@ private final class VideoRecorderImpl { private func maybeFinish() { dispatchPrecondition(condition: .onQueue(self.queue)) - guard self.hasAllVideoBuffers && self.hasAllVideoBuffers && !self.stopped else { + guard self.hasAllVideoBuffers && (!self.configuration.hasAudio || self.hasAllAudioBuffers) && !self.stopped else { return } let _ = self._stopped.modify { _ in return true } diff --git a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m index a0bb1df327..f92214b2b1 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m +++ b/submodules/LegacyComponents/Sources/TGPhotoCaptionInputMixin.m @@ -138,6 +138,10 @@ - (void)createDismissViewIfNeeded { + if (_dismissView != nil) { + return; + } + UIView *parentView = [self _parentView]; _dismissView = [[UIView alloc] initWithFrame:parentView.bounds]; @@ -231,6 +235,12 @@ - (void)finishEditing { if ([self.inputPanel dismissInput]) { _editing = false; + + [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ + _dismissView.alpha = 0.0f; + } completion:^(BOOL finished) { + + }]; if (self.finishedWithCaption != nil) self.finishedWithCaption([_inputPanel caption]); diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift index 44e310c99e..1e3adc0e28 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyListSettingsController.swift @@ -260,7 +260,11 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P entries.append(.serversHeader(theme, strings.SocksProxySetup_SavedProxies)) entries.append(.addServer(theme, strings.SocksProxySetup_AddProxy, state.editing)) var index = 0 + var existingServers = Set() for server in proxySettings.servers { + if !existingServers.insert(server).inserted { + continue + } let status: ProxyServerStatus = statuses[server] ?? .checking let displayStatus: DisplayProxyServerStatus if proxySettings.enabled && server == proxySettings.activeServer { @@ -301,7 +305,7 @@ private func proxySettingsControllerEntries(theme: PresentationTheme, strings: P entries.append(.server(index, theme, strings, server, server == proxySettings.activeServer, displayStatus, ProxySettingsServerItemEditing(editable: true, editing: state.editing, revealed: state.revealedServer == server), proxySettings.enabled)) index += 1 } - if !proxySettings.servers.isEmpty { + if !existingServers.isEmpty { entries.append(.shareProxyList(theme, strings.SocksProxySetup_ShareProxyList)) } diff --git a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift index 62f826c99e..560bd3fce2 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/ProxyServerSettingsController.swift @@ -347,8 +347,11 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: } } } else { - settings.servers.append(proxyServerSettings) - if settings.servers.count == 1 { + let wasEmpty = settings.servers.isEmpty + if !settings.servers.contains(proxyServerSettings) { + settings.servers.append(proxyServerSettings) + } + if wasEmpty && settings.servers.count == 1 { settings.activeServer = proxyServerSettings } } @@ -388,4 +391,3 @@ func proxyServerSettingsController(sharedContext: SharedAccountContext, context: return controller } - diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift index c1d2974745..c06460e8dc 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraVideoSource.swift @@ -34,7 +34,7 @@ final class CameraVideoSource: VideoSource { let index = self.onUpdatedListeners.add(f) return ActionDisposable { [weak self] in - DispatchQueue.main.async { + Queue.mainQueue().async { guard let self else { return } @@ -252,7 +252,7 @@ final class LiveStreamMediaSource { let index = self.onVideoUpdatedListeners.add(f) return ActionDisposable { [weak self] in - DispatchQueue.main.async { + Queue.mainQueue().async { guard let self else { return } diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 4a74571527..aace91290f 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -3427,6 +3427,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } } if let mediaDraftState = interfaceState.interfaceState.mediaDraftState, case .audio = mediaDraftState.contentType { + viewOnceIsVisible = true recordMoreIsVisible = true } @@ -3438,13 +3439,29 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg transition.updateSublayerTransformOffset(layer: self.clippingNode.layer, offset: CGPoint(x: 0.0, y: clippingDelta))*/ let viewOnceSize = self.viewOnceButton.update(theme: interfaceState.theme) - let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -152.0), size: viewOnceSize) + + var viewOnceButtonY: CGFloat = -105.0 + if isRecording { + if accessoryPanel == nil { + viewOnceButtonY -= 49.0 + } + } + + let viewOnceButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: viewOnceButtonY), size: viewOnceSize) self.viewOnceButton.bounds = CGRect(origin: .zero, size: viewOnceButtonFrame.size) transition.updatePosition(node: self.viewOnceButton, position: viewOnceButtonFrame.center) if self.viewOnceButton.alpha.isZero && viewOnceIsVisible { self.viewOnceButton.update(isSelected: self.viewOnce, animated: false) } + + transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) + transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) + if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil { + self.viewOnceButton.isHidden = false + } else { + self.viewOnceButton.isHidden = true + } let recordMoreSize = self.recordMoreButton.update(theme: interfaceState.theme) let recordMoreButtonFrame = CGRect(origin: CGPoint(x: width - rightInset - 50.0 - UIScreenPixel, y: -52.0), size: recordMoreSize) @@ -3455,14 +3472,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.recordMoreButton.update(isSelected: false, animated: false) } - transition.updateAlpha(node: self.viewOnceButton, alpha: viewOnceIsVisible ? 1.0 : 0.0) - transition.updateTransformScale(node: self.viewOnceButton, scale: viewOnceIsVisible ? 1.0 : 0.01) - if let user = interfaceState.renderedPeer?.peer as? TelegramUser, user.id != interfaceState.accountPeerId && user.botInfo == nil && interfaceState.sendPaidMessageStars == nil { - self.viewOnceButton.isHidden = false - } else { - self.viewOnceButton.isHidden = true - } - transition.updateAlpha(node: self.recordMoreButton, alpha: recordMoreIsVisible ? 1.0 : 0.0) transition.updateTransformScale(node: self.recordMoreButton, scale: recordMoreIsVisible ? 1.0 : 0.01) From 837acfa7842005c1c71f3bfb0c7f404099fb4d6a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 29 Apr 2026 04:26:58 +0200 Subject: [PATCH 3/5] Various fixes --- .../Sources/ChatTextInputPanelNode.swift | 3 --- .../ComposePollScreen/Sources/ComposePollScreen.swift | 3 ++- .../Sources/CountriesMultiselectionScreen.swift | 11 +++++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index aace91290f..fcb5d7a3cb 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -5610,9 +5610,6 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg } public func frameForInputActionButton() -> CGRect? { - if !self.sendActionButtons.alpha.isZero && self.sendActionButtons.frame.minX < self.bounds.width { - return self.sendActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) - } if !self.mediaActionButtons.alpha.isZero && self.mediaActionButtons.frame.minX < self.bounds.width { return self.mediaActionButtons.frame.insetBy(dx: 0.0, dy: -4.0).offsetBy(dx: -3.0, dy: 0.0) } diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index d365dfc98a..c6f4d2a4f4 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -1248,7 +1248,8 @@ final class ComposePollScreenComponent: Component { let stateContext = CountriesMultiselectionScreen.StateContext( context: component.context, subject: .countries, - initialSelectedCountries: self.limitToCountries + initialSelectedCountries: self.limitToCountries, + showFragment: true ) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self] _ in let controller = CountriesMultiselectionScreen( diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift index 5f2781e32d..769bd06411 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -1122,6 +1122,7 @@ public extension CountriesMultiselectionScreen { public let subject: Subject public let maxCount: Int32? public let initialSelectedCountries: [String] + public let showFragment: Bool private var stateDisposable: Disposable? private let stateSubject = Promise() @@ -1138,14 +1139,20 @@ public extension CountriesMultiselectionScreen { context: AccountContext, subject: Subject = .countries, maxCount: Int32? = nil, - initialSelectedCountries: [String] = [] + initialSelectedCountries: [String] = [], + showFragment: Bool = false ) { self.subject = subject self.maxCount = maxCount self.initialSelectedCountries = initialSelectedCountries + self.showFragment = showFragment let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let countries = localizedCountryNamesAndCodes(strings: presentationData.strings).sorted { lhs, rhs in + var countryList = localizedCountryNamesAndCodes(strings: presentationData.strings) + if showFragment { + countryList.append((("Fragment", "Fragment"), "FT", [888])) + } + let countries = countryList.sorted { lhs, rhs in return lhs.0.1.lowercased() < rhs.0.1.lowercased() } From 5d7edcf36f189691bd7bca8e98828459da687e91 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 29 Apr 2026 04:35:17 +0200 Subject: [PATCH 4/5] Various fixes --- .../Sources/ChatMessagePollBubbleContentNode.swift | 4 +++- .../ComposePollScreen/Sources/ComposePollScreen.swift | 6 +++++- .../Sources/CountriesMultiselectionScreen.swift | 2 +- .../TelegramUI/Sources/ChatInterfaceStateContextMenus.swift | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift index 6e87fb27b6..873a2a6dab 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessagePollBubbleContentNode/Sources/ChatMessagePollBubbleContentNode.swift @@ -2924,7 +2924,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode { if !poll.countries.isEmpty { let locale = localeWithStrings(item.presentationData.strings) let countryNames = poll.countries.map { id in - if let countryName = locale.localizedString(forRegionCode: id) { + if id == "FT" { + return "Fragment" + } else if let countryName = locale.localizedString(forRegionCode: id) { return countryName } else { return id diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index c6f4d2a4f4..d7b6393f44 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -2396,7 +2396,11 @@ final class ComposePollScreenComponent: Component { if self.limitToCountries.count > 1 { value = environment.strings.CreatePoll_AllowedCountries_Countries(Int32(self.limitToCountries.count)) } else if self.limitToCountries.count == 1, let countryCode = self.limitToCountries.first { - value = self.currentLocale?.localizedString(forRegionCode: countryCode) ?? countryCode + if countryCode == "FT" { + value = "Fragment" + } else { + value = self.currentLocale?.localizedString(forRegionCode: countryCode) ?? countryCode + } } else { value = "" } diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift index 769bd06411..4c8cda887a 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -1163,7 +1163,7 @@ public extension CountriesMultiselectionScreen { var currentSection: String? var currentCountries: [CountryItem] = [] for country in countries { - let section = String(country.0.1.prefix(1)) + let section = String(country.0.1.prefix(1)).uppercased() if currentSection != section { if let currentSection { sections.append((currentSection, currentCountries)) diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index e6a8b66608..ee3c2eeaee 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -2310,7 +2310,9 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if !poll.countries.isEmpty { let locale = localeWithStrings(chatPresentationInterfaceState.strings) let countryNames = poll.countries.map { id in - if let countryName = locale.localizedString(forRegionCode: id) { + if id == "FT" { + return "Fragment" + } else if let countryName = locale.localizedString(forRegionCode: id) { return countryName } else { return id From d25010c1cbfbacddf2482c30af805af5cb1dc1e7 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 29 Apr 2026 12:46:08 +0200 Subject: [PATCH 5/5] Various fixes --- .../State/AccountStateManagementUtils.swift | 18 ++++++++- .../TelegramEngine/Peers/RecentPeers.swift | 22 ++++++----- .../Sources/ComposePollScreen.swift | 8 ++++ .../CountriesMultiselectionScreen.swift | 15 ++++--- .../Sources/ChatHistoryListNode.swift | 39 +++++++++++-------- 5 files changed, 66 insertions(+), 36 deletions(-) diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index adc7c8fe68..4e0ca6e007 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -3309,7 +3309,7 @@ func resetChannels(accountPeerId: PeerId, postbox: Postbox, network: Network, pe resetForumTopics.insert(peerId) } - + for message in messages { var peerIsForum = false if let peerId = message.peerId { @@ -3943,6 +3943,7 @@ func replayFinalState( var updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:] var updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:] var updatedEmojiGameInfo: EmojiGameInfo? + var recentlyUsedGuestChatBots = Set() var holesFromPreviousStateMessageIds: [MessageId] = [] var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:] @@ -4341,6 +4342,15 @@ func replayFinalState( } } } + + if message.flags.contains(.Incoming), let authorId = message.authorId { + for attribute in message.attributes { + if let attribute = attribute as? GuestChatMessageAttribute, attribute.peerId == accountPeerId { + recentlyUsedGuestChatBots.insert(authorId) + break + } + } + } } if !message.flags.contains(.Incoming) && !message.flags.contains(.Unsent) { if message.id.peerId.namespace == Namespaces.Peer.CloudChannel { @@ -4350,7 +4360,7 @@ 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 { @@ -5867,6 +5877,10 @@ func replayFinalState( } } + for peerId in recentlyUsedGuestChatBots { + _internal_addRecentlyUsedInlineBot(transaction: transaction, peerId: peerId) + } + if syncAttachMenuBots { // addSynchronizeAttachMenuBotsOperation(transaction: transaction) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift index bee4cf3f35..fe9e694a9f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift @@ -211,17 +211,21 @@ func _internal_managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, return updatedRemotePeers } +func _internal_addRecentlyUsedInlineBot(transaction: Transaction, peerId: PeerId) { + var maxRating = 1.0 + for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots) { + if let contents = entry.contents.get(RecentPeerItem.self) { + maxRating = max(maxRating, contents.rating) + } + } + if let entry = CodableEntry(RecentPeerItem(rating: maxRating)) { + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, item: OrderedItemListEntry(id: RecentPeerItemId(peerId).rawValue, contents: entry), removeTailIfCountExceeds: 20) + } +} + func _internal_addRecentlyUsedInlineBot(postbox: Postbox, peerId: PeerId) -> Signal { return postbox.transaction { transaction -> Void in - var maxRating = 1.0 - for entry in transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots) { - if let contents = entry.contents.get(RecentPeerItem.self) { - maxRating = max(maxRating, contents.rating) - } - } - if let entry = CodableEntry(RecentPeerItem(rating: maxRating)) { - transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.CloudRecentInlineBots, item: OrderedItemListEntry(id: RecentPeerItemId(peerId).rawValue, contents: entry), removeTailIfCountExceeds: 20) - } + _internal_addRecentlyUsedInlineBot(transaction: transaction, peerId: peerId) } } diff --git a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift index d7b6393f44..d200bcaf60 100644 --- a/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift +++ b/submodules/TelegramUI/Components/ComposePollScreen/Sources/ComposePollScreen.swift @@ -1245,9 +1245,17 @@ final class ComposePollScreenComponent: Component { return } + let maxCount: Int32 + if let data = component.context.currentAppConfiguration.with({ $0 }).data, let value = data["poll_countries_max"] as? Double { + maxCount = Int32(value) + } else { + maxCount = 12 + } + let stateContext = CountriesMultiselectionScreen.StateContext( context: component.context, subject: .countries, + maxCount: maxCount, initialSelectedCountries: self.limitToCountries, showFragment: true ) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift index 4c8cda887a..3c3ad2573a 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CountriesMultiselectionScreen.swift @@ -752,7 +752,7 @@ final class CountriesMultiselectionScreenComponent: Component { if let searchStateContext = self.searchStateContext, searchStateContext.subject == .countriesSearch(query: self.navigationTextFieldState.text) { } else { self.searchStateDisposable?.dispose() - let searchStateContext = CountriesMultiselectionScreen.StateContext(context: component.context, subject: .countriesSearch(query: self.navigationTextFieldState.text)) + let searchStateContext = CountriesMultiselectionScreen.StateContext(context: component.context, subject: .countriesSearch(query: self.navigationTextFieldState.text), showFragment: component.stateContext.showFragment) var applyState = false self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -789,7 +789,6 @@ final class CountriesMultiselectionScreenComponent: Component { var sections: [ItemLayout.Section] = [] if let stateValue = self.effectiveStateValue { - var id: Int = 0 for (_, countries) in stateValue.sections { sections.append(ItemLayout.Section( @@ -1148,13 +1147,13 @@ public extension CountriesMultiselectionScreen { self.showFragment = showFragment let presentationData = context.sharedContext.currentPresentationData.with { $0 } - var countryList = localizedCountryNamesAndCodes(strings: presentationData.strings) - if showFragment { - countryList.append((("Fragment", "Fragment"), "FT", [888])) - } - let countries = countryList.sorted { lhs, rhs in + let countryList = localizedCountryNamesAndCodes(strings: presentationData.strings) + var countries = countryList.sorted { lhs, rhs in return lhs.0.1.lowercased() < rhs.0.1.lowercased() } + if showFragment, let index = countries.firstIndex(where: { $0.1 == "FR" }) { + countries.insert((("Fragment", "Fragment"), "FT", [888]), at: index) + } switch subject { case .countries: @@ -1164,7 +1163,7 @@ public extension CountriesMultiselectionScreen { var currentCountries: [CountryItem] = [] for country in countries { let section = String(country.0.1.prefix(1)).uppercased() - if currentSection != section { + if currentSection != section && country.1 != "FT" { if let currentSection { sections.append((currentSection, currentCountries)) } diff --git a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift index 089a3a00d6..8927a5af72 100644 --- a/submodules/TelegramUI/Sources/ChatHistoryListNode.swift +++ b/submodules/TelegramUI/Sources/ChatHistoryListNode.swift @@ -1724,27 +1724,32 @@ public final class ChatHistoryListNodeImpl: ListViewImpl, ChatHistoryNode, ChatH } |> distinctUntilChanged - let accountCountry: Signal = .single(nil) - |> then( - combineLatest( - accountPeer - |> map { peer -> String? in - if case let .user(user) = peer { - return user.phone - } else { + let accountCountry: Signal + if let data = context.currentAppConfiguration.with({ $0 }).data, let country = data["phone_country_iso2"] as? String { + accountCountry = .single(country) + } else { + accountCountry = .single(nil) + |> then( + combineLatest( + accountPeer + |> map { peer -> String? in + if case let .user(user) = peer { + return user.phone + } else { + return nil + } + } + |> distinctUntilChanged, + (context as! AccountContextImpl).countriesConfiguration + ) + |> map { phone, countriesConfiguration in + guard let phone, let (country, _) = lookupCountryIdByNumber(phone, configuration: countriesConfiguration) else { return nil } + return country.id } - |> distinctUntilChanged, - (context as! AccountContextImpl).countriesConfiguration ) - |> map { phone, countriesConfiguration in - guard let phone, let (country, _) = lookupCountryIdByNumber(phone, configuration: countriesConfiguration) else { - return nil - } - return country.id - } - ) + } let topicAuthorId: Signal if let peerId = chatLocation.peerId, let threadId = chatLocation.threadId {