diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index 8cb8cf30b8..1563a60a7c 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ 9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; }; 9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; }; 9DEC593B2D3002E70027CEBD /* AffineHttpHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */; }; - 9DEC593F2D30EFA40027CEBD /* AffineWsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC593E2D30EFA40027CEBD /* AffineWsHandler.swift */; }; 9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC59422D323EE00027CEBD /* Mutex.swift */; }; 9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */; }; C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; @@ -56,7 +55,6 @@ 9D90BE212CCB9876006677DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; 9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineHttpHandler.swift; sourceTree = ""; }; - 9DEC593E2D30EFA40027CEBD /* AffineWsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineWsHandler.swift; sourceTree = ""; }; 9DEC59422D323EE00027CEBD /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libaffine_mobile_native.a; sourceTree = ""; }; AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -164,7 +162,6 @@ children = ( 9DEC59422D323EE00027CEBD /* Mutex.swift */, 9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */, - 9DEC593E2D30EFA40027CEBD /* AffineWsHandler.swift */, 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */, 9D90BE1A2CCB9876006677DB /* Plugins */, 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */, @@ -349,7 +346,6 @@ 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */, 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */, 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */, - 9DEC593F2D30EFA40027CEBD /* AffineWsHandler.swift in Sources */, 9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */, 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */, ); diff --git a/packages/frontend/apps/ios/App/App/AffineViewController.swift b/packages/frontend/apps/ios/App/App/AffineViewController.swift index 357617a96a..eaffede75c 100644 --- a/packages/frontend/apps/ios/App/App/AffineViewController.swift +++ b/packages/frontend/apps/ios/App/App/AffineViewController.swift @@ -22,8 +22,6 @@ class AFFiNEViewController: CAPBridgeViewController { override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView { configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-http") configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-https") - configuration.setURLSchemeHandler(AffineWsHandler(), forURLScheme: "affine-ws") - configuration.setURLSchemeHandler(AffineWsHandler(), forURLScheme: "affine-wss") return super.webView(with: frame, configuration: configuration) } diff --git a/packages/frontend/apps/ios/App/App/AffineWsHandler.swift b/packages/frontend/apps/ios/App/App/AffineWsHandler.swift deleted file mode 100644 index 0dabbf0ba8..0000000000 --- a/packages/frontend/apps/ios/App/App/AffineWsHandler.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// RequestUrlSchemeHandler.swift -// App -// -// Created by EYHN on 2025/1/9. -// - -import WebKit - -enum AffineWsError: Error { - case invalidOperation(reason: String), invalidState(reason: String) -} - -/** - this custom url scheme handler simulates websocket connection through an http request. - frontend open websocket connections and send messages by sending requests to affine-ws:// or affine-wss:// - the handler has two endpoints: - `affine-ws:///open?uuid={uuid}&url={wsUrl}`: open a websocket connection and return received data through the SSE protocol. If the front-end closes the http connection, the websocket connection will also be closed. - `affine-ws:///send?uuid={uuid}`: send the request body data to the websocket connection with the specified uuid. - */ -class AffineWsHandler: NSObject, WKURLSchemeHandler { - var wsTasks: [UUID: URLSessionWebSocketTask] = [:] - func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { - urlSchemeTask.stopped = false - guard let rawUrl = urlSchemeTask.request.url else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "bad request")) - return - } - guard let urlComponents = URLComponents(url: rawUrl, resolvingAgainstBaseURL: true) else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "bad request")) - return - } - let path = urlComponents.path - if path == "/open" { - guard let targetUrlStr = urlComponents.queryItems?.first(where: { $0.name == "url" })?.value else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "url is request")) - return - } - - guard let targetUrl = URL(string: targetUrlStr) else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "failed to parse url")) - return - } - - guard let uuidStr = urlComponents.queryItems?.first(where: { $0.name == "uuid" })?.value else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "url is request")) - return - } - guard let uuid = UUID(uuidString: uuidStr) else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "invalid uuid")) - return - } - - guard let response = HTTPURLResponse.init(url: rawUrl, statusCode: 200, httpVersion: nil, headerFields: [ - "X-Accel-Buffering": "no", - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*" - ]) else { - urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "failed to create response")) - return - } - - urlSchemeTask.didReceive(response) - let jsonEncoder = JSONEncoder() - let json = String(data: try! jsonEncoder.encode(["type": "start"]), encoding: .utf8)! - urlSchemeTask.didReceive("data: \(json)\n\n".data(using: .utf8)!) - - var request = URLRequest(url: targetUrl); - request.httpShouldHandleCookies = true - - let webSocketTask = URLSession.shared.webSocketTask(with: targetUrl) - self.wsTasks[uuid] = webSocketTask - webSocketTask.resume() - - urlSchemeTask.wsTask = webSocketTask - - var completionHandler: ((Result) -> Void)! - completionHandler = { - let result = $0 - DispatchQueue.main.async { - if urlSchemeTask.stopped { - return - } - let jsonEncoder = JSONEncoder() - switch result { - case .success(let message): - if case .string(let string) = message { - let json = String(data: try! jsonEncoder.encode(["type": "message", "data": string]), encoding: .utf8)! - urlSchemeTask.didReceive("data: \(json)\n\n".data(using: .utf8)!) - } - case .failure(let error): - let json = String(data: try! jsonEncoder.encode(["type": "error", "error": error.localizedDescription]), encoding: .utf8)! - urlSchemeTask.didReceive("data: \(json)\n\n".data(using: .utf8)!) - urlSchemeTask.didFinish() - } - } - - // recursive calls - webSocketTask.receive(completionHandler: completionHandler) - } - - webSocketTask.receive(completionHandler: completionHandler) - } else if path == "/send" { - if urlSchemeTask.request.httpMethod != "POST" { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "Method should be POST")) - return - } - guard let uuidStr = urlComponents.queryItems?.first(where: { $0.name == "uuid" })?.value else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "url is request")) - return - } - guard let uuid = UUID(uuidString: uuidStr) else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "invalid uuid")) - return - } - guard let ContentType = urlSchemeTask.request.allHTTPHeaderFields?.first(where: {$0.key.lowercased() == "content-type"})?.value else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "content-type is request")) - return - } - if ContentType != "text/plain" { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "content-type not support")) - return - } - guard let body = urlSchemeTask.request.httpBody else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "no body")) - return - } - let stringBody = String(decoding: body, as: UTF8.self) - guard let webSocketTask = self.wsTasks[uuid] else { - urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "connection not found")) - return - } - - guard let response = HTTPURLResponse.init(url: rawUrl, statusCode: 200, httpVersion: nil, headerFields: [ - "Content-Type": "application/json", - "Cache-Control": "no-cache", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*" - ]) else { - urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "failed to create response")) - return - } - - let jsonEncoder = JSONEncoder() - - webSocketTask.send(.string(stringBody), completionHandler: { - error in - DispatchQueue.main.async { - if urlSchemeTask.stopped { - return - } - if error != nil { - let json = try! jsonEncoder.encode(["error": error!.localizedDescription]) - urlSchemeTask.didReceive(response) - urlSchemeTask.didReceive(json) - } else { - urlSchemeTask.didReceive(response) - urlSchemeTask.didReceive(try! jsonEncoder.encode(["uuid": uuid.uuidString.data(using: .utf8)!])) - urlSchemeTask.didFinish() - } - } - }) - } - } - - func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { - urlSchemeTask.stopped = true - urlSchemeTask.wsTask?.cancel(with: .abnormalClosure, reason: "Closed".data(using: .utf8)) - } -} - -private extension WKURLSchemeTask { - var stopped: Bool { - get { - return objc_getAssociatedObject(self, &stoppedKey) as? Bool ?? false - } - set { - objc_setAssociatedObject(self, &stoppedKey, newValue, .OBJC_ASSOCIATION_ASSIGN) - } - } - var wsTask: URLSessionWebSocketTask? { - get { - return objc_getAssociatedObject(self, &wsTaskKey) as? URLSessionWebSocketTask - } - set { - objc_setAssociatedObject(self, &wsTaskKey, newValue, .OBJC_ASSOCIATION_RETAIN) - } - } -} - -private var stoppedKey = malloc(1) -private var wsTaskKey = malloc(1) diff --git a/packages/frontend/apps/ios/App/App/InfoPlist.xcstrings b/packages/frontend/apps/ios/App/App/InfoPlist.xcstrings index 6cd6cdef4b..41abbd99a6 100644 --- a/packages/frontend/apps/ios/App/App/InfoPlist.xcstrings +++ b/packages/frontend/apps/ios/App/App/InfoPlist.xcstrings @@ -24,6 +24,30 @@ } } } + }, + "NSCameraUsageDescription" : { + "comment" : "Privacy - Camera Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "AFFiNE requires access to the camera to capture images and insert them into your documents" + } + } + } + }, + "NSPhotoLibraryUsageDescription" : { + "comment" : "Privacy - Photo Library Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "AFFiNE requires access to select photos from your photo library and insert them into your documents" + } + } + } } }, "version" : "1.0" diff --git a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift index 552f93b746..942f0688ed 100644 --- a/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift +++ b/packages/frontend/apps/ios/App/App/uniffi/affine_mobile_native.swift @@ -281,7 +281,7 @@ private func makeRustCall( _ callback: (UnsafeMutablePointer) -> T, errorHandler: ((RustBuffer) throws -> E)? ) throws -> T { - uniffiEnsureInitialized() + uniffiEnsureAffineMobileNativeInitialized() var callStatus = RustCallStatus.init() let returnedVal = callback(&callStatus) try uniffiCheckCallStatus(callStatus: callStatus, errorHandler: errorHandler) @@ -352,9 +352,10 @@ private func uniffiTraitInterfaceCallWithError( callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } } -fileprivate class UniffiHandleMap { - private var map: [UInt64: T] = [:] +fileprivate final class UniffiHandleMap: @unchecked Sendable { + // All mutation happens with this lock held, which is why we implement @unchecked Sendable. private let lock = NSLock() + private var map: [UInt64: T] = [:] private var currentHandle: UInt64 = 1 func insert(obj: T) -> UInt64 { @@ -496,7 +497,7 @@ fileprivate struct FfiConverterString: FfiConverter { -public protocol DocStoragePoolProtocol : AnyObject { +public protocol DocStoragePoolProtocol: AnyObject { func clearClocks(universalId: String) async throws @@ -554,9 +555,7 @@ public protocol DocStoragePoolProtocol : AnyObject { func setSpaceId(universalId: String, spaceId: String) async throws } - -open class DocStoragePool: - DocStoragePoolProtocol { +open class DocStoragePool: DocStoragePoolProtocol, @unchecked Sendable { fileprivate let pointer: UnsafeMutableRawPointer! /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. @@ -605,7 +604,7 @@ open class DocStoragePool: -open func clearClocks(universalId: String)async throws { +open func clearClocks(universalId: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -625,7 +624,7 @@ open func clearClocks(universalId: String)async throws { /** * Initialize the database and run migrations. */ -open func connect(universalId: String, path: String)async throws { +open func connect(universalId: String, path: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -642,7 +641,7 @@ open func connect(universalId: String, path: String)async throws { ) } -open func deleteBlob(universalId: String, key: String, permanently: Bool)async throws { +open func deleteBlob(universalId: String, key: String, permanently: Bool)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -659,7 +658,7 @@ open func deleteBlob(universalId: String, key: String, permanently: Bool)async t ) } -open func deleteDoc(universalId: String, docId: String)async throws { +open func deleteDoc(universalId: String, docId: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -676,7 +675,7 @@ open func deleteDoc(universalId: String, docId: String)async throws { ) } -open func disconnect(universalId: String)async throws { +open func disconnect(universalId: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -693,7 +692,7 @@ open func disconnect(universalId: String)async throws { ) } -open func getBlob(universalId: String, key: String)async throws -> Blob? { +open func getBlob(universalId: String, key: String)async throws -> Blob? { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -710,7 +709,7 @@ open func getBlob(universalId: String, key: String)async throws -> Blob? { ) } -open func getDocClock(universalId: String, docId: String)async throws -> DocClock? { +open func getDocClock(universalId: String, docId: String)async throws -> DocClock? { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -727,7 +726,7 @@ open func getDocClock(universalId: String, docId: String)async throws -> DocClo ) } -open func getDocClocks(universalId: String, after: Int64?)async throws -> [DocClock] { +open func getDocClocks(universalId: String, after: Int64?)async throws -> [DocClock] { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -744,7 +743,7 @@ open func getDocClocks(universalId: String, after: Int64?)async throws -> [DocC ) } -open func getDocSnapshot(universalId: String, docId: String)async throws -> DocRecord? { +open func getDocSnapshot(universalId: String, docId: String)async throws -> DocRecord? { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -761,7 +760,7 @@ open func getDocSnapshot(universalId: String, docId: String)async throws -> Doc ) } -open func getDocUpdates(universalId: String, docId: String)async throws -> [DocUpdate] { +open func getDocUpdates(universalId: String, docId: String)async throws -> [DocUpdate] { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -778,7 +777,7 @@ open func getDocUpdates(universalId: String, docId: String)async throws -> [Doc ) } -open func getPeerPulledRemoteClock(universalId: String, peer: String, docId: String)async throws -> DocClock? { +open func getPeerPulledRemoteClock(universalId: String, peer: String, docId: String)async throws -> DocClock? { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -795,7 +794,7 @@ open func getPeerPulledRemoteClock(universalId: String, peer: String, docId: Str ) } -open func getPeerPulledRemoteClocks(universalId: String, peer: String)async throws -> [DocClock] { +open func getPeerPulledRemoteClocks(universalId: String, peer: String)async throws -> [DocClock] { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -812,7 +811,7 @@ open func getPeerPulledRemoteClocks(universalId: String, peer: String)async thro ) } -open func getPeerPushedClock(universalId: String, peer: String, docId: String)async throws -> DocClock? { +open func getPeerPushedClock(universalId: String, peer: String, docId: String)async throws -> DocClock? { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -829,7 +828,7 @@ open func getPeerPushedClock(universalId: String, peer: String, docId: String)as ) } -open func getPeerPushedClocks(universalId: String, peer: String)async throws -> [DocClock] { +open func getPeerPushedClocks(universalId: String, peer: String)async throws -> [DocClock] { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -846,7 +845,7 @@ open func getPeerPushedClocks(universalId: String, peer: String)async throws -> ) } -open func getPeerRemoteClock(universalId: String, peer: String, docId: String)async throws -> DocClock? { +open func getPeerRemoteClock(universalId: String, peer: String, docId: String)async throws -> DocClock? { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -863,7 +862,7 @@ open func getPeerRemoteClock(universalId: String, peer: String, docId: String)as ) } -open func getPeerRemoteClocks(universalId: String, peer: String)async throws -> [DocClock] { +open func getPeerRemoteClocks(universalId: String, peer: String)async throws -> [DocClock] { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -880,7 +879,7 @@ open func getPeerRemoteClocks(universalId: String, peer: String)async throws -> ) } -open func listBlobs(universalId: String)async throws -> [ListedBlob] { +open func listBlobs(universalId: String)async throws -> [ListedBlob] { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -897,7 +896,7 @@ open func listBlobs(universalId: String)async throws -> [ListedBlob] { ) } -open func markUpdatesMerged(universalId: String, docId: String, updates: [Int64])async throws -> UInt32 { +open func markUpdatesMerged(universalId: String, docId: String, updates: [Int64])async throws -> UInt32 { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -914,7 +913,7 @@ open func markUpdatesMerged(universalId: String, docId: String, updates: [Int64] ) } -open func pushUpdate(universalId: String, docId: String, update: String)async throws -> Int64 { +open func pushUpdate(universalId: String, docId: String, update: String)async throws -> Int64 { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -931,7 +930,7 @@ open func pushUpdate(universalId: String, docId: String, update: String)async th ) } -open func releaseBlobs(universalId: String)async throws { +open func releaseBlobs(universalId: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -948,13 +947,13 @@ open func releaseBlobs(universalId: String)async throws { ) } -open func setBlob(universalId: String, blob: SetBlob)async throws { +open func setBlob(universalId: String, blob: SetBlob)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_affine_mobile_native_fn_method_docstoragepool_set_blob( self.uniffiClonePointer(), - FfiConverterString.lower(universalId),FfiConverterTypeSetBlob.lower(blob) + FfiConverterString.lower(universalId),FfiConverterTypeSetBlob_lower(blob) ) }, pollFunc: ffi_affine_mobile_native_rust_future_poll_void, @@ -965,13 +964,13 @@ open func setBlob(universalId: String, blob: SetBlob)async throws { ) } -open func setDocSnapshot(universalId: String, snapshot: DocRecord)async throws -> Bool { +open func setDocSnapshot(universalId: String, snapshot: DocRecord)async throws -> Bool { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_affine_mobile_native_fn_method_docstoragepool_set_doc_snapshot( self.uniffiClonePointer(), - FfiConverterString.lower(universalId),FfiConverterTypeDocRecord.lower(snapshot) + FfiConverterString.lower(universalId),FfiConverterTypeDocRecord_lower(snapshot) ) }, pollFunc: ffi_affine_mobile_native_rust_future_poll_i8, @@ -982,7 +981,7 @@ open func setDocSnapshot(universalId: String, snapshot: DocRecord)async throws ) } -open func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Int64)async throws { +open func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Int64)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -999,7 +998,7 @@ open func setPeerPulledRemoteClock(universalId: String, peer: String, docId: Str ) } -open func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Int64)async throws { +open func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Int64)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -1016,7 +1015,7 @@ open func setPeerPushedClock(universalId: String, peer: String, docId: String, c ) } -open func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Int64)async throws { +open func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Int64)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -1033,7 +1032,7 @@ open func setPeerRemoteClock(universalId: String, peer: String, docId: String, c ) } -open func setSpaceId(universalId: String, spaceId: String)async throws { +open func setSpaceId(universalId: String, spaceId: String)async throws { return try await uniffiRustCallAsync( rustFutureFunc: { @@ -1053,6 +1052,7 @@ open func setSpaceId(universalId: String, spaceId: String)async throws { } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1088,8 +1088,6 @@ public struct FfiConverterTypeDocStoragePool: FfiConverter { } - - #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1105,6 +1103,8 @@ public func FfiConverterTypeDocStoragePool_lower(_ value: DocStoragePool) -> Uns } + + public struct Blob { public var key: String public var data: String @@ -1123,6 +1123,9 @@ public struct Blob { } } +#if compiler(>=6) +extension Blob: Sendable {} +#endif extension Blob: Equatable, Hashable { @@ -1155,6 +1158,7 @@ extension Blob: Equatable, Hashable { } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1207,6 +1211,9 @@ public struct DocClock { } } +#if compiler(>=6) +extension DocClock: Sendable {} +#endif extension DocClock: Equatable, Hashable { @@ -1227,6 +1234,7 @@ extension DocClock: Equatable, Hashable { } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1275,6 +1283,9 @@ public struct DocRecord { } } +#if compiler(>=6) +extension DocRecord: Sendable {} +#endif extension DocRecord: Equatable, Hashable { @@ -1299,6 +1310,7 @@ extension DocRecord: Equatable, Hashable { } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1349,6 +1361,9 @@ public struct DocUpdate { } } +#if compiler(>=6) +extension DocUpdate: Sendable {} +#endif extension DocUpdate: Equatable, Hashable { @@ -1373,6 +1388,7 @@ extension DocUpdate: Equatable, Hashable { } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1425,6 +1441,9 @@ public struct ListedBlob { } } +#if compiler(>=6) +extension ListedBlob: Sendable {} +#endif extension ListedBlob: Equatable, Hashable { @@ -1453,6 +1472,7 @@ extension ListedBlob: Equatable, Hashable { } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1505,6 +1525,9 @@ public struct SetBlob { } } +#if compiler(>=6) +extension SetBlob: Sendable {} +#endif extension SetBlob: Equatable, Hashable { @@ -1529,6 +1552,7 @@ extension SetBlob: Equatable, Hashable { } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1627,14 +1651,32 @@ public struct FfiConverterTypeUniffiError: FfiConverterRustBuffer { } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUniffiError_lift(_ buf: RustBuffer) throws -> UniffiError { + return try FfiConverterTypeUniffiError.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeUniffiError_lower(_ value: UniffiError) -> RustBuffer { + return FfiConverterTypeUniffiError.lower(value) +} + + extension UniffiError: Equatable, Hashable {} + + extension UniffiError: Foundation.LocalizedError { public var errorDescription: String? { String(reflecting: self) } } + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -1843,9 +1885,9 @@ fileprivate func uniffiRustCallAsync( liftFunc: (F) throws -> T, errorHandler: ((RustBuffer) throws -> Swift.Error)? ) async throws -> T { - // Make sure to call uniffiEnsureInitialized() since future creation doesn't have a + // Make sure to call the ensure init function since future creation doesn't have a // RustCallStatus param, so doesn't use makeRustCall() - uniffiEnsureInitialized() + uniffiEnsureAffineMobileNativeInitialized() let rustFuture = rustFutureFunc() defer { freeFunc(rustFuture) @@ -1876,7 +1918,7 @@ fileprivate func uniffiFutureContinuationCallback(handle: UInt64, pollResult: In print("uniffiFutureContinuationCallback invalid handle") } } -public func hashcashMint(resource: String, bits: UInt32) -> String { +public func hashcashMint(resource: String, bits: UInt32) -> String { return try! FfiConverterString.lift(try! rustCall() { uniffi_affine_mobile_native_fn_func_hashcash_mint( FfiConverterString.lower(resource), @@ -1884,8 +1926,8 @@ public func hashcashMint(resource: String, bits: UInt32) -> String { ) }) } -public func newDocStoragePool() -> DocStoragePool { - return try! FfiConverterTypeDocStoragePool.lift(try! rustCall() { +public func newDocStoragePool() -> DocStoragePool { + return try! FfiConverterTypeDocStoragePool_lift(try! rustCall() { uniffi_affine_mobile_native_fn_func_new_doc_storage_pool($0 ) }) @@ -1898,9 +1940,9 @@ private enum InitializationResult { } // Use a global variable to perform the versioning checks. Swift ensures that // the code inside is only computed once. -private var initializationResult: InitializationResult = { +private let initializationResult: InitializationResult = { // Get the bindings contract version from our ComponentInterface - let bindings_contract_version = 26 + let bindings_contract_version = 29 // Get the scaffolding contract version by calling the into the dylib let scaffolding_contract_version = ffi_affine_mobile_native_uniffi_contract_version() if bindings_contract_version != scaffolding_contract_version { @@ -1994,7 +2036,9 @@ private var initializationResult: InitializationResult = { return InitializationResult.ok }() -private func uniffiEnsureInitialized() { +// Make the ensure init function public so that other modules which have external type references to +// our types can call it. +public func uniffiEnsureAffineMobileNativeInitialized() { switch initializationResult { case .ok: break diff --git a/packages/frontend/apps/ios/src/setup.ts b/packages/frontend/apps/ios/src/setup.ts index f9fec4569b..a554e4b0ce 100644 --- a/packages/frontend/apps/ios/src/setup.ts +++ b/packages/frontend/apps/ios/src/setup.ts @@ -1,7 +1,7 @@ import '@affine/core/bootstrap/browser'; /** - * the below code includes the custom fetch and websocket implementation for ios webview. + * the below code includes the custom fetch and xmlhttprequest implementation for ios webview. * should be included in the entry file of the app or webworker. */ @@ -38,154 +38,25 @@ globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { return rawFetch(url, input instanceof Request ? input : init); }; -/** - * we create a custom websocket class to simulate the browser's websocket connection - * through the custom url scheme handler. - * - * to overcome the restrictions of cross-domain and third-party cookies in ios webview, - * the front-end opens a websocket connection and sends a message by sending a request - * to `affine-ws://` or `affine-wss://`. - * - * the scheme has two endpoints: - * - * `affine-ws:///open?uuid={uuid}&url={wsUrl}`: opens a websocket connection and returns - * the received data via the SSE protocol. - * If the front-end closes the http connection, the websocket connection will also be closed. - * - * `affine-ws:///send?uuid={uuid}`: sends the request body data to the websocket connection - * with the specified uuid. - */ -class WrappedWebSocket { - static CLOSED = WebSocket.CLOSED; - static CLOSING = WebSocket.CLOSING; - static CONNECTING = WebSocket.CONNECTING; - static OPEN = WebSocket.OPEN; - readonly isWss: boolean; - readonly uuid = crypto.randomUUID(); - readyState: number = WebSocket.CONNECTING; - events: Record void)[]> = {}; - onopen: ((event: any) => void) | undefined = undefined; - onclose: ((event: any) => void) | undefined = undefined; - onerror: ((event: any) => void) | undefined = undefined; - onmessage: ((event: any) => void) | undefined = undefined; - eventSource: EventSource; - constructor( - readonly url: string, - _protocols?: string | string[] // not supported yet +const rawXMLHttpRequest = globalThis.XMLHttpRequest; +globalThis.XMLHttpRequest = class extends rawXMLHttpRequest { + override open( + method: string, + url: string, + async?: boolean, + user?: string, + password?: string ) { - const parsedUrl = new URL(url); - this.isWss = parsedUrl.protocol === 'wss:'; - this.eventSource = new EventSource( - `${this.isWss ? 'affine-wss' : 'affine-ws'}:///open?uuid=${this.uuid}&url=${encodeURIComponent(this.url)}` - ); - this.eventSource.addEventListener('open', () => { - this.emitOpen(new Event('open')); - }); - this.eventSource.addEventListener('error', () => { - this.eventSource.close(); - this.emitError(new Event('error')); - this.emitClose(new CloseEvent('close')); - }); - this.eventSource.addEventListener('message', data => { - const decodedData = JSON.parse(data.data); - if (decodedData.type === 'message') { - this.emitMessage( - new MessageEvent('message', { data: decodedData.data }) - ); - } - }); - } + let normalizedUrl = new URL(url, globalThis.location.origin).href; - send(data: string) { - rawFetch( - `${this.isWss ? 'affine-wss' : 'affine-ws'}:///send?uuid=${this.uuid}`, - { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - }, - body: data, - } - ).catch(e => { - console.error('Failed to send message', e); - }); - } - - close() { - this.eventSource.close(); - this.emitClose(new CloseEvent('close')); - } - - addEventListener(type: string, listener: (event: any) => void) { - this.events[type] = this.events[type] || []; - this.events[type].push(listener); - } - - removeEventListener(type: string, listener: (event: any) => void) { - this.events[type] = this.events[type] || []; - this.events[type] = this.events[type].filter(l => l !== listener); - } - - private emitOpen(event: Event) { - this.readyState = WebSocket.OPEN; - this.events['open']?.forEach(listener => { - try { - listener(event); - } catch (e) { - console.error(e); - } - }); - try { - this.onopen?.(event); - } catch (e) { - console.error(e); + if (normalizedUrl.startsWith('http:')) { + url = 'affine-http:' + url.slice(5); } - } - private emitClose(event: CloseEvent) { - this.readyState = WebSocket.CLOSED; - this.events['close']?.forEach(listener => { - try { - listener(event); - } catch (e) { - console.error(e); - } - }); - try { - this.onclose?.(event); - } catch (e) { - console.error(e); + if (normalizedUrl.startsWith('https:')) { + url = 'affine-https:' + url.slice(6); } - } - private emitMessage(event: MessageEvent) { - this.events['message']?.forEach(listener => { - try { - listener(event); - } catch (e) { - console.error(e); - } - }); - try { - this.onmessage?.(event); - } catch (e) { - console.error(e); - } + (super.open as any)(method, url, async, user, password); } - - private emitError(event: Event) { - this.events['error']?.forEach(listener => { - try { - listener(event); - } catch (e) { - console.error(e); - } - }); - try { - this.onerror?.(event); - } catch (e) { - console.error(e); - } - } -} -globalThis.WebSocket = WrappedWebSocket as any; +};