Experiment

This commit is contained in:
Isaac 2026-02-05 14:58:39 +08:00
parent 8e9cd79855
commit 8005726b3d
9 changed files with 216 additions and 31 deletions

View file

@ -57,6 +57,7 @@ swift_library(
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
"//submodules/TelegramUI/Components/GlassControls",
"//submodules/UIKitRuntimeUtils",
],
visibility = [
"//visibility:public",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
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

View file

@ -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<NSNumber *> *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<NSString *, TrustedWebRecord *> *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<NSString *> * _Nonnull)threadSafeTrustedDomains {
[webHelpersLock() lock];
NSMutableArray<NSString *> *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();
}

View file

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