From 8005726b3da8a9821729144422c2ccbfc5f70b5f Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Thu, 5 Feb 2026 14:58:39 +0800 Subject: [PATCH] Experiment --- submodules/BrowserUI/BUILD | 1 + .../BrowserUI/Sources/BrowserWebContent.swift | 40 +++++- .../Sources/DebugController.swift | 42 +++++-- .../Sources/LegacyBuffer.swift | 15 ++- .../Sources/ExperimentalUISettings.swift | 10 +- submodules/UIKitRuntimeUtils/BUILD | 5 +- .../UIViewController+Navigation.h | 8 ++ .../UIViewController+Navigation.m | 118 ++++++++++++++++++ submodules/WebUI/Sources/WebAppWebView.swift | 8 +- 9 files changed, 216 insertions(+), 31 deletions(-) diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 472c755041..fc57ca47eb 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -57,6 +57,7 @@ swift_library( "//submodules/TelegramUI/Components/GlassBarButtonComponent", "//submodules/TelegramUI/Components/SearchInputPanelComponent", "//submodules/TelegramUI/Components/GlassControls", + "//submodules/UIKitRuntimeUtils", ], visibility = [ "//visibility:public", diff --git a/submodules/BrowserUI/Sources/BrowserWebContent.swift b/submodules/BrowserUI/Sources/BrowserWebContent.swift index 617ed64ed7..9bb574d5f7 100644 --- a/submodules/BrowserUI/Sources/BrowserWebContent.swift +++ b/submodules/BrowserUI/Sources/BrowserWebContent.swift @@ -24,6 +24,7 @@ import DeviceModel import LegacyMediaPickerUI import PassKit import AlertComponent +import UIKitRuntimeUtils private final class TonSchemeHandler: NSObject, WKURLSchemeHandler { private final class PendingTask { @@ -217,6 +218,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU var cancelInteractiveTransitionGestures: () -> Void = {} private var tempFile: TempBoxFile? + private var disposeTrustedDomain: (() -> Void)? init(context: AccountContext, presentationData: PresentationData, url: String, preferredConfiguration: WKWebViewConfiguration? = nil) { self.context = context @@ -264,6 +266,30 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU configuration.userContentController = contentController configuration.applicationNameForUserAgent = computedUserAgent() } + + if context.sharedContext.immediateExperimentalUISettings.enablePWA { + if #available(iOS 17.0, *) { + if let parsedUrl = URL(string: url), let host = parsedUrl.host { + let rootPath = context.sharedContext.applicationBindings.containerPath + "/telegram-data" + let pwaPath = rootPath + "/pwa" + let uuidPath = pwaPath + "/uuid_\(host)" + let uuid: UUID + if let value = try? String(contentsOf: URL(fileURLWithPath: uuidPath), encoding: .utf8) { + uuid = UUID(uuidString: value)! + } else { + uuid = UUID() + let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: pwaPath), withIntermediateDirectories: true) + let _ = try? uuid.uuidString.write(to: URL(fileURLWithPath: uuidPath), atomically: true, encoding: .utf8) + } + + configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: uuid) + + configuration.limitsNavigationsToAppBoundDomains = true + disposeTrustedDomain = WebHelpers.addTrustedDomain(host) + WebHelpers.forceRefreshTrustedDomains(configuration.websiteDataStore) + } + } + } self.webView = WebView(frame: CGRect(), configuration: configuration) self.webView.allowsLinkPreview = true @@ -273,18 +299,16 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU } var title: String = "" + var request: URLRequest? if url.hasPrefix("file://") { var updatedPath = url let tempFile = TempBox.shared.file(path: url.replacingOccurrences(of: "file://", with: ""), fileName: "file.xlsx") updatedPath = tempFile.path self.tempFile = tempFile - let request = URLRequest(url: URL(fileURLWithPath: updatedPath)) - self.webView.load(request) + request = URLRequest(url: URL(fileURLWithPath: updatedPath)) } else if let parsedUrl = URL(string: url) { - let request = URLRequest(url: parsedUrl) - self.webView.load(request) - + request = URLRequest(url: parsedUrl) title = getDisplayUrl(url, hostOnly: true) } @@ -299,6 +323,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.webView.backgroundColor = presentationData.theme.list.plainBackgroundColor self.webView.alpha = 0.0 + if let request { + self.webView.load(request) + } + self.webView.allowsBackForwardNavigationGestures = true self.webView.scrollView.delegate = self self.webView.scrollView.clipsToBounds = false @@ -360,6 +388,8 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU self.faviconDisposable.dispose() self.instantPageDisposable.dispose() + + self.disposeTrustedDomain?() } private func handleScriptMessage(_ message: WKScriptMessage) { diff --git a/submodules/DebugSettingsUI/Sources/DebugController.swift b/submodules/DebugSettingsUI/Sources/DebugController.swift index 27725379af..afc8356b4f 100644 --- a/submodules/DebugSettingsUI/Sources/DebugController.swift +++ b/submodules/DebugSettingsUI/Sources/DebugController.swift @@ -109,7 +109,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case playerV2(Bool) case devRequests(Bool) case enableUpdates(Bool) - case fakeAds(Bool) + case pwa(Bool) case enableLocalTranslation(Bool) case preferredVideoCodec(Int, String, String?, Bool) case disableVideoAspectScaling(Bool) @@ -135,7 +135,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.web.rawValue case .keepChatNavigationStack, .skipReadHistory, .alwaysDisplayTyping, .debugRatingLayout, .crashOnSlowQueries, .crashOnMemoryPressure: return DebugControllerSection.experiments.rawValue - case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .fakeGlass, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .enableUpdates, .fakeAds, .enableLocalTranslation: + case .clearTips, .resetNotifications, .crash, .fillLocalSavedMessageCache, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .resetTagHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .compressedEmojiCache, .storiesJpegExperiment, .checkSerializedData, .enableQuickReactionSwitch, .experimentalCompatibility, .enableDebugDataDisplay, .fakeGlass, .browserExperiment, .allForumsHaveTabs, .enableReactionOverrides, .restorePurchases, .disableReloginTokens, .liveStreamV2, .experimentalCallMute, .playerV2, .devRequests, .enableUpdates, .pwa, .enableLocalTranslation: return DebugControllerSection.experiments.rawValue case .logTranslationRecognition, .resetTranslationStates: return DebugControllerSection.translation.rawValue @@ -258,7 +258,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 54 case .devRequests: return 55 - case .fakeAds: + case .pwa: return 56 case .enableLocalTranslation: return 57 @@ -1200,7 +1200,18 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) case .resetWebViewCache: return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: "Clear Web View Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: { - WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ }) + WKWebsiteDataStore.default().removeData(ofTypes: [ + WKWebsiteDataTypeDiskCache, + WKWebsiteDataTypeOfflineWebApplicationCache, + WKWebsiteDataTypeMemoryCache, + WKWebsiteDataTypeLocalStorage, + WKWebsiteDataTypeCookies, + WKWebsiteDataTypeSessionStorage, + WKWebsiteDataTypeIndexedDBDatabases, + WKWebsiteDataTypeWebSQLDatabases, + WKWebsiteDataTypeFetchCache, + WKWebsiteDataTypeServiceWorkerRegistrations + ], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ }) }) case .optimizeDatabase: return ItemListActionItem(presentationData: presentationData, systemStyle: .glass, title: "Optimize Database", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { @@ -1392,12 +1403,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { }) }).start() }) - case let .fakeAds(value): - return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Fake Ads", value: value, sectionId: self.section, style: .blocks, updated: { value in + case let .pwa(value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Test1", value: value, sectionId: self.section, style: .blocks, updated: { value in let _ = arguments.sharedContext.accountManager.transaction ({ transaction in transaction.updateSharedData(ApplicationSpecificSharedDataKeys.experimentalUISettings, { settings in var settings = settings?.get(ExperimentalUISettings.self) ?? ExperimentalUISettings.defaultSettings - settings.fakeAds = value + settings.enablePWA = value return PreferencesEntry(settings) }) }).start() @@ -1488,7 +1499,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { } } -private func debugControllerEntries(sharedContext: SharedAccountContext, presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings, experimentalSettings: ExperimentalUISettings, networkSettings: NetworkSettings?, hasLegacyAppData: Bool, useBetaFeatures: Bool) -> [DebugControllerEntry] { +private func debugControllerEntries(context: AccountContext?, sharedContext: SharedAccountContext, presentationData: PresentationData, loggingSettings: LoggingSettings, mediaInputSettings: MediaInputSettings, experimentalSettings: ExperimentalUISettings, networkSettings: NetworkSettings?, hasLegacyAppData: Bool, useBetaFeatures: Bool) -> [DebugControllerEntry] { var entries: [DebugControllerEntry] = [] let isMainApp = sharedContext.applicationBindings.isMainApp @@ -1572,7 +1583,18 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present entries.append(.playerV2(experimentalSettings.playerV2)) entries.append(.devRequests(experimentalSettings.devRequests)) - entries.append(.fakeAds(experimentalSettings.fakeAds)) + + if let data = context?.currentAppConfiguration.with({ $0 }).data { + var displayPwa = false + if let _ = data["ios_display_pwa"] { + displayPwa = true + } else if let isDev = data["dev"] as? Double, isDev == 1.0 { + displayPwa = true + } + if displayPwa { + entries.append(.pwa(experimentalSettings.enablePWA)) + } + } entries.append(.enableLocalTranslation(experimentalSettings.enableLocalTranslation)) entries.append(.enableUpdates(experimentalSettings.enableUpdates)) } @@ -1658,7 +1680,7 @@ public func debugController(sharedContext: SharedAccountContext, context: Accoun } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Debug"), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) - let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: debugControllerEntries(sharedContext: sharedContext, presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings, experimentalSettings: experimentalSettings, networkSettings: networkSettings, hasLegacyAppData: hasLegacyAppData, useBetaFeatures: useBetaFeatures), style: .blocks) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: debugControllerEntries(context: context, sharedContext: sharedContext, presentationData: presentationData, loggingSettings: loggingSettings, mediaInputSettings: mediaInputSettings, experimentalSettings: experimentalSettings, networkSettings: networkSettings, hasLegacyAppData: hasLegacyAppData, useBetaFeatures: useBetaFeatures), style: .blocks) return (controllerState, (listState, arguments)) } diff --git a/submodules/LegacyDataImport/Sources/LegacyBuffer.swift b/submodules/LegacyDataImport/Sources/LegacyBuffer.swift index edafb9bbea..cebc964f39 100644 --- a/submodules/LegacyDataImport/Sources/LegacyBuffer.swift +++ b/submodules/LegacyDataImport/Sources/LegacyBuffer.swift @@ -166,13 +166,16 @@ class LegacyBufferReader { if count == 0 { return 0 } - else if count > 0 && count <= 4 || self.offset + UInt(count) <= self.buffer._size { - var value: Int32 = 0 - memcpy(&value, self.buffer.data?.advanced(by: Int(self.offset)), count) - self.offset += UInt(count) - return value + guard count > 0, count <= 4, self.offset + UInt(count) <= self.buffer._size else { + return nil } - return nil + guard let bufferData = self.buffer.data else { + return nil + } + var value: Int32 = 0 + memcpy(&value, bufferData.advanced(by: Int(self.offset)), count) + self.offset += UInt(count) + return value } func readBuffer(_ count: Int) -> LegacyBuffer? { diff --git a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift index 77f4684457..4bf6e9ec9a 100644 --- a/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift +++ b/submodules/TelegramUIPreferences/Sources/ExperimentalUISettings.swift @@ -69,6 +69,7 @@ public struct ExperimentalUISettings: Codable, Equatable { public var allForumsHaveTabs: Bool public var debugRatingLayout: Bool public var enableUpdates: Bool + public var enablePWA: Bool public static var defaultSettings: ExperimentalUISettings { return ExperimentalUISettings( @@ -115,7 +116,8 @@ public struct ExperimentalUISettings: Codable, Equatable { checkSerializedData: false, allForumsHaveTabs: false, debugRatingLayout: false, - enableUpdates: false + enableUpdates: false, + enablePWA: false ) } @@ -163,7 +165,8 @@ public struct ExperimentalUISettings: Codable, Equatable { checkSerializedData: Bool, allForumsHaveTabs: Bool, debugRatingLayout: Bool, - enableUpdates: Bool + enableUpdates: Bool, + enablePWA: Bool ) { self.keepChatNavigationStack = keepChatNavigationStack self.skipReadHistory = skipReadHistory @@ -209,6 +212,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.allForumsHaveTabs = allForumsHaveTabs self.debugRatingLayout = debugRatingLayout self.enableUpdates = enableUpdates + self.enablePWA = enablePWA } public init(from decoder: Decoder) throws { @@ -258,6 +262,7 @@ public struct ExperimentalUISettings: Codable, Equatable { self.allForumsHaveTabs = try container.decodeIfPresent(Bool.self, forKey: "allForumsHaveTabs") ?? false self.debugRatingLayout = try container.decodeIfPresent(Bool.self, forKey: "debugRatingLayout") ?? false self.enableUpdates = try container.decodeIfPresent(Bool.self, forKey: "enableUpdates") ?? false + self.enablePWA = try container.decodeIfPresent(Bool.self, forKey: "enablePWA") ?? false } public func encode(to encoder: Encoder) throws { @@ -307,6 +312,7 @@ public struct ExperimentalUISettings: Codable, Equatable { try container.encodeIfPresent(self.allForumsHaveTabs, forKey: "allForumsHaveTabs") try container.encodeIfPresent(self.debugRatingLayout, forKey: "debugRatingLayout") try container.encodeIfPresent(self.enableUpdates, forKey: "enableUpdates") + try container.encodeIfPresent(self.enablePWA, forKey: "enablePWA") } } diff --git a/submodules/UIKitRuntimeUtils/BUILD b/submodules/UIKitRuntimeUtils/BUILD index 50ef9fac7d..96f6226f73 100644 --- a/submodules/UIKitRuntimeUtils/BUILD +++ b/submodules/UIKitRuntimeUtils/BUILD @@ -16,12 +16,13 @@ objc_library( "Source", ], deps = [ - "//submodules/AsyncDisplayKit:AsyncDisplayKit", - "//submodules/ObjCRuntimeUtils:ObjCRuntimeUtils", + "//submodules/AsyncDisplayKit", + "//submodules/ObjCRuntimeUtils", ], sdk_frameworks = [ "Foundation", "UIKit", + "WebKit", ], visibility = [ "//visibility:public", diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h index 928aa420c9..dab3ef6624 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h @@ -1,4 +1,5 @@ #import +#import typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { UIResponderDisableAutomaticKeyboardHandlingForward = 1 << 0, @@ -104,3 +105,10 @@ void snapshotViewByDrawingInContext(UIView * _Nonnull view); @property (nonatomic) double lumaMax; @end + +@interface WebHelpers : NSObject + ++ (dispatch_block_t _Nonnull)addTrustedDomain:(NSString * _Nonnull)domain; ++ (void)forceRefreshTrustedDomains:(WKWebsiteDataStore * _Nonnull)websiteDataStore; + +@end diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 2c4f273a4e..3be2d1b264 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -405,6 +405,122 @@ static void registerEffectViewOverrides(void) { } } +static NSLock *webHelpersLock() { + static NSLock *value = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + value = [[NSLock alloc] init]; + }); + return value; +} + +@interface TrustedWebRecord : NSObject + +@property (nonatomic) NSInteger nextReference; +@property (nonatomic, strong, readonly) NSMutableSet *references; + +@end + +@implementation TrustedWebRecord + +- (instancetype)init { + self = [super init]; + if (self != nil) { + _references = [[NSMutableSet alloc] init]; + } + return self; +} + +- (NSInteger)addReference { + NSInteger reference = _nextReference; + _nextReference += 1; + [_references addObject:@(reference)]; + return reference; +} + +- (void)removeReference:(NSInteger)reference { + [_references removeObject:@(reference)]; +} + +- (bool)isEmpty { + return _references.count == 0; +} + +@end + +static NSMutableDictionary *trustedWebRecords() { + static NSMutableDictionary *value = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + value = [[NSMutableDictionary alloc] init]; + }); + return value; +} + +void WKWebsiteDataStoreReinitializeAppBoundDomains(CFTypeRef dataStoreRef); + +@implementation WebHelpers + ++ (NSArray * _Nonnull)threadSafeTrustedDomains { + [webHelpersLock() lock]; + + NSMutableArray *result = [[NSMutableArray alloc] init]; + for (NSString *domain in trustedWebRecords()) { + [result addObject:domain]; + } + [result sortUsingSelector:@selector(compare:)]; + + [webHelpersLock() unlock]; + + return result; +} + ++ (dispatch_block_t _Nonnull)addTrustedDomain:(NSString * _Nonnull)domain { + [webHelpersLock() lock]; + + TrustedWebRecord *record = trustedWebRecords()[domain]; + if (record == nil) { + record = [[TrustedWebRecord alloc] init]; + trustedWebRecords()[domain] = record; + } + NSInteger reference = [record addReference]; + __block __weak TrustedWebRecord *weakRecord = record; + + [webHelpersLock() unlock]; + + return ^{ + [webHelpersLock() lock]; + + TrustedWebRecord *strongRecord = weakRecord; + if (strongRecord && trustedWebRecords()[domain] == strongRecord) { + [strongRecord removeReference:reference]; + if ([strongRecord isEmpty]) { + [trustedWebRecords() removeObjectForKey:domain]; + } + } + + [webHelpersLock() unlock]; + }; +} + ++ (void)forceRefreshTrustedDomains:(WKWebsiteDataStore * _Nonnull)websiteDataStore { + WKWebsiteDataStoreReinitializeAppBoundDomains((__bridge CFTypeRef)(websiteDataStore)); +} + +@end + +@implementation NSBundle (Telegram) + +- (id)_65087dc8_objectForInfoDictionaryKey:(NSString *)key { + if ([key isEqualToString:@"WKAppBoundDomains"]) { + //NSLog(@"Returning trusted domains: %@", [WebHelpers threadSafeTrustedDomains]); + return [WebHelpers threadSafeTrustedDomains]; + } + return [self _65087dc8_objectForInfoDictionaryKey:key]; +} + +@end + @implementation UIViewController (Navigation) + (void)load @@ -432,6 +548,8 @@ static void registerEffectViewOverrides(void) { [RuntimeUtils swizzleInstanceMethodOfClass:[UIFocusSystem class] currentSelector:@selector(updateFocusIfNeeded) newSelector:@selector(_65087dc8_updateFocusIfNeeded)]; + [RuntimeUtils swizzleInstanceMethodOfClass:[NSBundle class] currentSelector:@selector(objectForInfoDictionaryKey:) newSelector:@selector(_65087dc8_objectForInfoDictionaryKey:)]; + if (@available(iOS 26.0, *)) { registerEffectViewOverrides(); } diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 2c82e6c57a..29ef625b2b 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -162,12 +162,8 @@ final class WebAppWebView: WKWebView { self.isOpaque = false self.backgroundColor = .clear - if #available(iOS 9.0, *) { - self.allowsLinkPreview = false - } - if #available(iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } + self.allowsLinkPreview = false + self.scrollView.contentInsetAdjustmentBehavior = .never self.interactiveTransitionGestureRecognizerTest = { point -> Bool in return point.x > 30.0 }