Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2026-04-29 20:27:28 +02:00
commit 5c2cebc7bf
13 changed files with 233 additions and 92 deletions

View file

@ -16155,6 +16155,41 @@ Error: %8$@";
"TextProcessing.ResultBadge" = "Result";
"TextProcessing.Translate.LanguageStyle" = "%1$@ (%2$@)";
"TextProcessing.StyleTooltip" = "Select Style";
"TextProcessing.AlertCreatorDeleteStyle.Title" = "Delete Style";
"TextProcessing.AlertCreatorDeleteStyle.Text" = "Are you sure you want to delete this style? It will be removed for everyone who installed it.";
"TextProcessing.AlertDeleteStyle.Title" = "Delete Style";
"TextProcessing.AlertDeleteStyle.Text" = "Are you sure you want to delete this style?";
"TextProcessing.StyleMenu.Edit" = "Edit Style";
"TextProcessing.StyleMenu.Share" = "Share Style";
"TextProcessing.StyleMenu.Delete" = "Delete Style";
"TextProcessing.StyleMenu.ButtonClose" = "Close";
"TextProcessing.StyleMenu.ButtonAdd" = "Add Style";
"TextProcessing.StylePreview.ExampleHeader" = "EXAMPLE";
"TextProcessing.StylePreview.ExampleHeaderRefresh" = "ANOTHER EXAMPLE";
"TextProcessing.StylePreview.Subtitle" = "Add this style to instantly\nrewrite your messages.";
"TextProcessing.StylePreview.Before" = "Before";
"TextProcessing.StylePreview.After" = "After";
"TextProcessing.AlertTooManyStyles.Title" = "Too Many Styles";
"TextProcessing.AlertTooManyStyles.Text" = "Please delete some of your saved styles to create a new one.";
"TextProcessing.ToastStyleCreated.Title" = "%@ style created!";
"TextProcessing.ToastStyleCreated.Text" = "Press and hold a style to edit or share the link.";
"TextProcessing.StyleList.Add" = "Add Style";
"TextProcessing.StyleFooterAuthor" = "Style by [%@]()";
"TextProcessing.StyleFooterUserCount_1" = "Used by 1 person";
"TextProcessing.StyleFooterUserCount_any" = "Used by %d people";
"TextProcessing.StyleFooterCreatedByFormat" = "%1$@. %2$@";
"TextProcessing.StyleFooterCreatedBy" = "Created by [%@]()";
"TextProcessing.StyleFooterCreatedBySimpleFormat" = "%@.";
"TextProcessing.EditStyle.NamePlaceholder" = "Style Name (for example, \"Pirate\")";
"TextProcessing.EditStyle.TextPlaceholder" = "Instructions (for example, \"Write like a swashbuckling pirate. Use arr, ye, matey, and talk about treasure, the sea, and rum\")";
"TextProcessing.EditStyle.TitleCreate" = "New Style";
"TextProcessing.EditStyle.TitleEdit" = "Edit Style";
"TextProcessing.EditStyle.ActionCreate" = "Create";
"TextProcessing.EditStyle.ActionEdit" = "Save";
"TextProcessing.EditStyle.Delete" = "Delete Style";
"TextProcessing.EditStyle.AddLink" = "Add a link to my account";
"TextProcessing.ToastStyleAdded.Title" = "Style Added";
"TextProcessing.ToastStyleAdded.Text" = "Tap 'AI' → '%@' when typing your next long message.";
"Bot.AlertCanNotCreateBots" = "%@ can't manage other bots.";
@ -16264,3 +16299,9 @@ Error: %8$@";
"ChatbotSetup.SetupNotCompleted.Title" = "No Bot Added";
"ChatbotSetup.SetupNotCompleted.Text" = "You havent added a bot to manage your account. Leave anyway?";
"ChatbotSetup.SetupNotCompleted.Leave" = "Leave";
"Chat.SavedMessagesStatusViewAsChats" = "Tap to view as chats";
"Chat.ToastVoiceMessageDeviceMuted" = "Device is muted.";
"VideoChat.StatusPeerJoined" = "%@ joined";
"VideoChat.StatusPeerLeft" = "%@ left";

View file

@ -1069,28 +1069,30 @@ final class MutableMessageHistoryView: MutablePostboxView {
}
}
switch self.peerIds {
case let .single(peerId, threadId):
let location = PeerAndThreadId(peerId: peerId, threadId: threadId)
if let typingDraftUpdate = transaction.updatedTypingDrafts[location] {
if let typingDraft = typingDraftUpdate.value, self.namespaces.contains(typingDraft.namespace) {
self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft)
} else {
self.typingDraft = nil
if self.tag == nil {
switch self.peerIds {
case let .single(peerId, threadId):
let location = PeerAndThreadId(peerId: peerId, threadId: threadId)
if let typingDraftUpdate = transaction.updatedTypingDrafts[location] {
if let typingDraft = typingDraftUpdate.value, self.namespaces.contains(typingDraft.namespace) {
self.typingDraft = self.renderTypingDraft(postbox: postbox, typingDraft: typingDraft)
} else {
self.typingDraft = nil
}
hasChanges = true
}
hasChanges = true
case .external:
break
case .associated:
break
}
case .external:
break
case .associated:
break
}
return hasChanges
}
private func reloadTypingDraft(postbox: PostboxImpl) {
guard case let .single(peerId, threadId) = self.peerIds else {
guard case let .single(peerId, threadId) = self.peerIds, self.tag == nil else {
self.typingDraft = nil
return
}

View file

@ -1911,11 +1911,10 @@ final class VideoChatScreenComponent: Component {
}
}
} else {
//TODO:localized
if event.joined {
self.lastTitleEvent = "\(event.peer.compactDisplayTitle) joined"
self.lastTitleEvent = environment.strings.VideoChat_StatusPeerJoined(event.peer.compactDisplayTitle).string
} else {
self.lastTitleEvent = "\(event.peer.compactDisplayTitle) left"
self.lastTitleEvent = environment.strings.VideoChat_StatusPeerLeft(event.peer.compactDisplayTitle).string
}
if !self.isUpdating {
self.state?.updated(transition: .spring(duration: 0.4))

View file

@ -2806,6 +2806,15 @@ func extractEmojiFileIds(message: StoreMessage, fileIds: inout Set<Int64>) {
}
}
}
for media in message.media {
if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content {
for attribute in content.attributes {
if case let .aiTextStyle(aiTextStyle) = attribute {
fileIds.insert(aiTextStyle.emojiFileId)
}
}
}
}
}
private func messagesFromOperations(state: AccountMutableState) -> [StoreMessage] {

View file

@ -7,15 +7,6 @@ struct Rectangle {
float2 size;
};
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
struct QuadVertexOut {
float4 position [[position]];
float2 uv;

View file

@ -310,8 +310,8 @@ final class TextProcessingContentComponent: Component {
if style.isCreator {
environment.controller()?.push(textAlertController(
context: component.context,
title: "Delete Style",
text: "Are you sure you want to delete this style? It will be removed for everyone who installed it.",
title: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Title,
text: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Text,
actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in
@ -326,8 +326,8 @@ final class TextProcessingContentComponent: Component {
} else {
environment.controller()?.push(textAlertController(
context: component.context,
title: "Delete Style",
text: "Are you sure you want to delete this style?",
title: environment.strings.TextProcessing_AlertDeleteStyle_Title,
text: environment.strings.TextProcessing_AlertDeleteStyle_Text,
actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in
@ -354,7 +354,7 @@ final class TextProcessingContentComponent: Component {
var items: [ContextMenuItem] = []
if style.isAuthor {
items.append(.action(ContextMenuActionItem(
text: "Edit Style",
text: environment.strings.TextProcessing_StyleMenu_Edit,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.contextMenu.primaryColor)
},
@ -369,7 +369,7 @@ final class TextProcessingContentComponent: Component {
))
}
items.append(.action(ContextMenuActionItem(
text: "Share Style",
text: environment.strings.TextProcessing_StyleMenu_Share,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
},
@ -382,23 +382,21 @@ final class TextProcessingContentComponent: Component {
})
})
))
if style.isAuthor {
items.append(.action(ContextMenuActionItem(
text: "Delete Style",
textColor: .destructive,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
},
action: { [weak self] c, _ in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.requestDeleteStyle(id: id)
})
items.append(.action(ContextMenuActionItem(
text: environment.strings.TextProcessing_StyleMenu_Delete,
textColor: .destructive,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
},
action: { [weak self] c, _ in
c?.dismiss(completion: { [weak self] in
guard let self else {
return
}
self.requestDeleteStyle(id: id)
})
))
}
})
))
final class ContextExtractedContentSourceImpl: ContextExtractedContentSource {
let keepInPlace: Bool = false
@ -578,7 +576,7 @@ final class TextProcessingContentComponent: Component {
let descriptionSize = previewDescription.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Add this style to instantly\nrewrite your messages.", font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)),
text: .plain(NSAttributedString(string: environment.strings.TextProcessing_StylePreview_Subtitle, font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor)),
horizontalAlignment: .center,
maximumNumberOfLines: 0,
lineSpacing: 0.12
@ -807,7 +805,6 @@ final class TextProcessingContentComponent: Component {
if component.styles.filter({ $0.isAuthor }).count >= maxStyles {
if !hasPremium {
//TODO:localize
let context = component.context
var replaceImpl: ((ViewController) -> Void)?
let controller = context.sharedContext.makePremiumDemoController(context: context, subject: .aiTools, forceDark: false, action: {
@ -826,8 +823,8 @@ final class TextProcessingContentComponent: Component {
} else {
environment.controller()?.push(textAlertController(
context: component.context,
title: "Too Many Styles",
text: "Please delete some of your saved styles to create a new one.",
title: environment.strings.TextProcessing_AlertTooManyStyles_Title,
text: environment.strings.TextProcessing_AlertTooManyStyles_Text,
actions: [
TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {}),
]
@ -1437,8 +1434,7 @@ private final class TextProcessingSheetComponent: Component {
dismiss(true)
}
case let .preview(style, _, _, isAlreadyAdded, added):
//TODO:localize
actionButtonTitle = isAlreadyAdded ? "Done" : "Add Style"
actionButtonTitle = isAlreadyAdded ? environmentValue.strings.TextProcessing_StyleMenu_ButtonClose : environmentValue.strings.TextProcessing_StyleMenu_ButtonAdd
isMainActionEnabled = !self.isPerformingMainAction
performMainAction = { [weak self] in
guard let self, let component = self.component else {
@ -1799,10 +1795,10 @@ private final class TextProcessingSheetComponent: Component {
)),
content: AnyComponent(VStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "\(styleCreatedToastData.style.title) style created!", font: Font.semibold(14.0), textColor: .white)),
text: .plain(NSAttributedString(string: environmentValue.strings.TextProcessing_ToastStyleCreated_Title(styleCreatedToastData.style.title).string, font: Font.semibold(14.0), textColor: .white)),
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .markdown(text: "Press and hold a style to edit or share the link.", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })),
text: .markdown(text: environmentValue.strings.TextProcessing_ToastStyleCreated_Text, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })),
maximumNumberOfLines: 0
)))
], alignment: .left, spacing: 6.0)),

View file

@ -638,11 +638,10 @@ private final class CreateItemComponent: Component {
containerSize: CGSize(width: 100.0, height: 100.0)
)
//TODO:localize
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Add Style", font: Font.medium(10.0), textColor: iconTintColor))
text: .plain(NSAttributedString(string: component.strings.TextProcessing_StyleList_Add, font: Font.medium(10.0), textColor: iconTintColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 100.0)

View file

@ -382,9 +382,8 @@ final class TextProcessingTranslateContentComponent: Component {
}
toTitle = ""
case .preview:
//TODO:localize
fromFormat = "Before"
toFormat = "After"
fromFormat = component.strings.TextProcessing_StylePreview_Before
toFormat = component.strings.TextProcessing_StylePreview_After
toTitle = ""
}
@ -407,14 +406,12 @@ final class TextProcessingTranslateContentComponent: Component {
}
if case .stylize = component.mode {
//TODO:localize
if case .style = component.externalState.style, let style = component.styles.first(where: { $0.id == component.externalState.style }), let authorPeer = style.authorPeer {
let footerText: String
//TODO:localize
if let addressName = authorPeer.addressName {
footerText = "Style by [@\(addressName)](\(authorPeer.id.toInt64()))"
footerText = component.strings.TextProcessing_StyleFooterAuthor("@" + addressName).string
} else {
footerText = "Style by [\(authorPeer.displayTitle(strings: component.strings, displayOrder: .firstLast))](\(authorPeer.id.toInt64()))"
footerText = component.strings.TextProcessing_StyleFooterAuthor(authorPeer.displayTitle(strings: component.strings, displayOrder: .firstLast)).string
}
component.externalState.sectionFooter = AnyComponentWithIdentity(id: "style_by_\(authorPeer.id.toInt64())", component: AnyComponent(MultilineTextComponent(
text: .markdown(text: footerText, attributes: MarkdownAttributes(
@ -448,7 +445,7 @@ final class TextProcessingTranslateContentComponent: Component {
} else if case let .preview(_, _, authorPeer, userCount, _) = component.mode {
component.externalState.sectionHeader = AnyComponentWithIdentity(id: "preview", component: AnyComponent(HStack([
AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent(
text: .markdown(text: "EXAMPLE", attributes: MarkdownAttributes(
text: .markdown(text: component.strings.TextProcessing_StylePreview_ExampleHeader, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.freeTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: component.theme.list.freeTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor),
@ -464,7 +461,7 @@ final class TextProcessingTranslateContentComponent: Component {
tintColor: component.theme.list.itemAccentColor
))),
AnyComponentWithIdentity(id: 1, component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "ANOTHER EXAMPLE", font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor))
text: .plain(NSAttributedString(string: component.strings.TextProcessing_StylePreview_ExampleHeaderRefresh, font: Font.regular(13.0), textColor: component.theme.list.itemAccentColor))
)))
], spacing: 2.0)),
action: { [weak self] in
@ -478,18 +475,17 @@ final class TextProcessingTranslateContentComponent: Component {
))),
], spacing: 8.0, alignment: .alternatingLeftRight)))
let userCountString = userCount == 1 ? "Used by 1 person" : "Used by \(userCount) people"
let userCountString = component.strings.TextProcessing_StyleFooterUserCount(Int32(userCount))
let footerText: String
//TODO:localize
if let authorPeer {
if let addressName = authorPeer.addressName {
footerText = "\(userCountString). Created by [@\(addressName)](\(authorPeer.id.toInt64()))"
footerText = component.strings.TextProcessing_StyleFooterCreatedByFormat(userCountString, component.strings.TextProcessing_StyleFooterCreatedBy("@" + addressName).string).string
} else {
footerText = "\(userCountString). Created by [\(authorPeer.displayTitle(strings: component.strings, displayOrder: .firstLast))](\(authorPeer.id.toInt64()))"
footerText = component.strings.TextProcessing_StyleFooterCreatedByFormat(userCountString, component.strings.TextProcessing_StyleFooterCreatedBy(authorPeer.displayTitle(strings: component.strings, displayOrder: .firstLast)).string).string
}
} else {
footerText = "\(userCountString)."
footerText = component.strings.TextProcessing_StyleFooterCreatedBySimpleFormat(userCountString).string
}
component.externalState.sectionFooter = AnyComponentWithIdentity(id: "style_by_\(authorPeer?.id.toInt64() ?? 0)", component: AnyComponent(MultilineTextComponent(
text: .markdown(text: footerText, attributes: MarkdownAttributes(

View file

@ -335,7 +335,6 @@ final class TextStyleEditContentComponent: Component {
contentHeight += iconBackgroundSize.height + iconSpacing
//TODO:localize
var titleSectionItems: [AnyComponentWithIdentity<Empty>] = []
titleSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: component.externalState.titleInputState,
@ -347,7 +346,7 @@ final class TextStyleEditContentComponent: Component {
resetText: resetTitle.flatMap { resetTitle in
return ListMultilineTextFieldItemComponent.ResetText(value: resetTitle)
},
placeholder: "Style Name (for example, \"Pirate\")",
placeholder: environment.strings.TextProcessing_EditStyle_NamePlaceholder,
autocapitalizationType: .sentences,
autocorrectionType: .default,
characterLimit: 12,
@ -379,7 +378,6 @@ final class TextStyleEditContentComponent: Component {
contentHeight += titleSectionSize.height
contentHeight += sectionSpacing
//TODO:localize
var textSectionItems: [AnyComponentWithIdentity<Empty>] = []
textSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
externalState: component.externalState.textInputState,
@ -391,7 +389,7 @@ final class TextStyleEditContentComponent: Component {
resetText: resetText.flatMap { resetText in
return ListMultilineTextFieldItemComponent.ResetText(value: resetText)
},
placeholder: "Instructions (for example, \"Write like a swashbuckling pirate. Use arr, ye, matey, and talk about treasure, the sea, and rum\")",
placeholder: environment.strings.TextProcessing_EditStyle_TextPlaceholder,
placeholderDefinesMinHeight: true,
autocapitalizationType: .sentences,
autocorrectionType: .default,
@ -439,7 +437,7 @@ final class TextStyleEditContentComponent: Component {
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Delete Style",
string: environment.strings.TextProcessing_EditStyle_Delete,
font: Font.regular(17.0),
textColor: environment.theme.list.itemDestructiveColor
)),
@ -453,8 +451,8 @@ final class TextStyleEditContentComponent: Component {
}
environment.controller()?.push(textAlertController(
context: component.context,
title: "Delete Style",
text: "Are you sure you want to delete this style? It will be removed for everyone who installed it.",
title: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Title,
text: environment.strings.TextProcessing_AlertCreatorDeleteStyle_Text,
actions: [
TextAlertAction(type: .genericAction, title: environment.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: environment.strings.Common_Delete, action: { [weak self] in
@ -503,7 +501,7 @@ final class TextStyleEditContentComponent: Component {
selected: component.externalState.isLinkToProfileEnabled
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "Add a link to my account", font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor))
text: .plain(NSAttributedString(string: environment.strings.TextProcessing_EditStyle_AddLink, font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor))
)))
], spacing: 10.0)),
effectAlignment: .center,
@ -779,11 +777,11 @@ private final class TextStyleEditSheetComponent: Component {
let titleString: String
switch component.mode {
case .create:
titleString = "New Style"
actionButtonTitle = "Create"
titleString = environmentValue.strings.TextProcessing_EditStyle_TitleCreate
actionButtonTitle = environmentValue.strings.TextProcessing_EditStyle_ActionCreate
case .edit:
titleString = "Edit Style"
actionButtonTitle = "Save"
titleString = environmentValue.strings.TextProcessing_EditStyle_TitleEdit
actionButtonTitle = environmentValue.strings.TextProcessing_EditStyle_ActionEdit
}
let sheetSize = self.sheet.update(
@ -889,7 +887,7 @@ public class TextStyleEditScreen: ViewControllerComponentContainer {
var initialEmojiFile: TelegramMediaFile?
if case let .edit(style) = mode, case let .custom(style) = style.content, let emojiFileId = style.emojiFileId {
initialEmojiFile = await context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]).get()[emojiFileId]
initialEmojiFile = await context.engine.stickers.resolveInlineStickersLocal(fileIds: [emojiFileId]).get()[emojiFileId]
}
super.init(

View file

@ -609,8 +609,7 @@ extension ChatControllerImpl {
} else {
var customSubtitle: String?
if savedMessagesChatsTip {
//TODO:localize
customSubtitle = "Tap to view as chats"
customSubtitle = strings.Chat_SavedMessagesStatusViewAsChats
}
strongSelf.state.chatTitleContent = .peer(peerView: ChatTitleContent.PeerData(peerView: peerView), customTitle: nil, customSubtitle: customSubtitle, onlineMemberCount: onlineMemberCount, isScheduledMessages: isScheduledMessages, isMuted: nil, customMessageCount: nil, hidePeerStatus: false, isEnabled: hasPeerInfo)

View file

@ -330,8 +330,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
} |> take(1) |> timeout(1.0, queue: .mainQueue(), alternate: .single(false)) |> deliverOnMainQueue).startStandalone(next: { value in
if value {
let presentationData = params.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
let toastController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: "Device is muted.", timeout: 4.0, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
let toastController = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Chat_ToastVoiceMessageDeviceMuted, timeout: 4.0, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return true
})
params.present(toastController, nil, .current)

View file

@ -2054,8 +2054,7 @@ func openResolvedUrlImpl(
guard let emojiFileId = style.emojiFileId, let file = await context.engine.stickers.resolveInlineStickers(fileIds: [emojiFileId]).get().first?.value else {
return
}
//TODO:localize
present(UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: context, file: file, loop: false, title: "Style Added", text: "Tap 'AI' → '\(style.title)' when typing your next long message.", undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
present(UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: context, file: file, loop: false, title: presentationData.strings.TextProcessing_ToastStyleAdded_Title, text: presentationData.strings.TextProcessing_ToastStyleAdded_Text(style.title).string, undoText: nil, customAction: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in
return true
}), nil)
}

View file

@ -1601,7 +1601,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.controller?._isPanGestureEnabled = isPanGestureEnabled
}
case "web_app_share_to_story":
if let json, let mediaUrl = json["media_url"] as? String {
if let json, let mediaUrl = json["media_url"] as? String, isAllowedBotMediaUrl(mediaUrl) {
let text = json["text"] as? String
let link = json["widget_link"] as? [String: Any]
@ -4309,3 +4309,116 @@ private struct WebAppConfiguration {
}
}
}
private func isAllowedBotMediaUrl(_ urlString: String) -> Bool {
guard let escaped = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: escaped) else {
return false
}
guard url.scheme?.lowercased() == "https" else {
return false
}
if url.user != nil || url.password != nil {
return false
}
guard var host = url.host?.lowercased(), !host.isEmpty else {
return false
}
if host.hasPrefix("[") && host.hasSuffix("]") {
host = String(host.dropFirst().dropLast())
}
// Strict canonical dotted-decimal IPv4 (4 octets, no leading zeros, each 0-255).
// Do NOT use inet_pton here: Darwin's inet_pton accepts "0177.0.0.1" as
// decimal 177.0.0.1, but getaddrinfo (used by URLSession) interprets the
// same string as octal 127.0.0.1 the divergence is a loopback bypass.
if let v4Bytes = parseCanonicalIPv4(host) {
return isPublicIPv4(v4Bytes)
}
// IPv6 only host must contain ":" so we don't accidentally hand a
// numeric-looking hostname to inet_pton.
if host.contains(":") {
var v6 = in6_addr()
if host.withCString({ inet_pton(AF_INET6, $0, &v6) }) == 1 {
let bytes = withUnsafeBytes(of: &v6) { ptr -> [UInt8] in
return Array(ptr)
}
return isPublicIPv6(bytes)
}
return false
}
// Strict DNS-name validation. Anything that doesn't look like a real
// FQDN is rejected this catches non-canonical numeric IP forms
// (decimal-32 like "2130706433", octal like "0177.0.0.1", hex like
// "0x7f.0.0.1", short forms like "127.1") that the OS resolver may
// still treat as 127.0.0.1 even when inet_pton would accept them as
// a different value or reject outright.
let labels = host.split(separator: ".", omittingEmptySubsequences: false)
guard labels.count >= 2 else { return false }
for label in labels {
guard !label.isEmpty, label.count <= 63 else { return false }
if label.first == "-" || label.last == "-" { return false }
for ch in label {
guard ch.isASCII else { return false }
if !(ch.isLetter || ch.isNumber || ch == "-") { return false }
}
}
guard let tld = labels.last, tld.count >= 2, tld.contains(where: { $0.isLetter }) else {
return false
}
if host == "localhost" || host.hasSuffix(".localhost") || host.hasSuffix(".local") {
return false
}
return true
}
private func parseCanonicalIPv4(_ host: String) -> [UInt8]? {
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count == 4 else { return nil }
var bytes: [UInt8] = []
bytes.reserveCapacity(4)
for part in parts {
guard !part.isEmpty, part.count <= 3 else { return nil }
if part.count > 1 && part.first == "0" { return nil } // no leading zeros (octal-spoof)
guard part.allSatisfy({ $0.isASCII && $0.isNumber }) else { return nil }
guard let value = UInt8(part) else { return nil } // also caps at 255
bytes.append(value)
}
return bytes
}
private func isPublicIPv4(_ bytes: [UInt8]) -> Bool {
guard bytes.count == 4 else { return false }
let a = bytes[0]
let b = bytes[1]
if a == 0 { return false } // 0.0.0.0/8
if a == 10 { return false } // 10.0.0.0/8
if a == 127 { return false } // 127.0.0.0/8 loopback
if a == 169 && b == 254 { return false } // 169.254.0.0/16 link-local
if a == 172 && (b & 0xf0) == 16 { return false } // 172.16.0.0/12
if a == 192 && b == 168 { return false } // 192.168.0.0/16
if a == 100 && (b & 0xc0) == 64 { return false } // 100.64.0.0/10 CGNAT
if a >= 224 { return false } // multicast + reserved + 255.255.255.255
return true
}
private func isPublicIPv6(_ bytes: [UInt8]) -> Bool {
guard bytes.count == 16 else { return false }
if bytes.allSatisfy({ $0 == 0 }) { return false } // ::
let loopback: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]
if bytes == loopback { return false } // ::1
if bytes[0] == 0xff { return false } // ff00::/8 multicast
if bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0x80 { return false } // fe80::/10 link-local
if (bytes[0] & 0xfe) == 0xfc { return false } // fc00::/7 unique-local
let v4MappedPrefix: [UInt8] = [0,0,0,0,0,0,0,0,0,0,0xff,0xff]
if Array(bytes.prefix(12)) == v4MappedPrefix { // ::ffff:a.b.c.d
return isPublicIPv4(Array(bytes.suffix(4)))
}
if Array(bytes.prefix(12)).allSatisfy({ $0 == 0 }) { // ::a.b.c.d (deprecated)
return isPublicIPv4(Array(bytes.suffix(4)))
}
return true
}