Various improvements

This commit is contained in:
Ilya Laktyushin 2026-04-28 23:53:26 +02:00
parent 92c350b559
commit ea090a6858
15 changed files with 168 additions and 70 deletions

View file

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

View file

@ -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: #"</?([A-Za-z][A-Za-z0-9:-]*)\b[^>]*?>"#)
@ -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 {

View file

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

View file

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

View file

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

View file

@ -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<Int>) -> Void)?
fileprivate let valuePromise = ValuePromise<NSAttributedString>(NSAttributedString())
public var valueSignal: Signal<NSAttributedString, NoError> {
return self.valuePromise.get()
@ -32,6 +33,13 @@ public final class AlertMultilineInputFieldComponent: Component {
public init() {
}
public func setValue(_ value: NSAttributedString, selectionRange: Range<Int>? = 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<AlertComponentEnvironment>, 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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