ios: implement window.confirm()/alert() in the thread feed webview (#106)
FeedWebCache never set a uiDelegate on the per-thread embedded webview at all, so WKWebView silently no-ops window.confirm()/alert(). chat.js's renderApp()/openThread() are shared between embed and full-app mode, so the "Delete this message" confirm (reachable via long-press in the embedded feed) was doing nothing on iOS — the same gap the Mac app had in ChatFeedWebView.swift. Add WKUIDelegate conformance to WebBridge with async runJavaScriptConfirmPanelWithMessage/AlertPanelWithMessage, presenting a UIAlertController from the topmost view controller, and wire it up via web.uiDelegate = bridge in FeedWebCache.makeEntry.
Co-authored-by: Claude <noreply@anthropic.com>
1 file changed,
+35
−0
ios/LChat/Views/ChatView.swift+35 −0
diff --git a/ios/LChat/Views/ChatView.swift b/ios/LChat/Views/ChatView.swift| index 70817ca..3a2e623 100644 |
| --- a/ios/LChat/Views/ChatView.swift |
| +++ b/ios/LChat/Views/ChatView.swift |
| @@ -551,6 +551,41 @@ final class WebBridge: NSObject, ObservableObject, WKScriptMessageHandler, WKNav |
| "window.lchatNative && window.lchatNative.didAttach && window.lchatNative.didAttach(\(force))") |
| } |
| + // WKUIDelegate shows nothing for window.confirm()/alert() unless we implement |
| + // these — chat.js's "Delete this message" confirm (reachable here since embed mode |
| + // still shares renderApp()/openThread() with the full app) silently no-ops without |
| + // them. The Mac app had the identical gap; see ChatFeedWebView.swift there. |
| + func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, |
| + initiatedByFrame frame: WKFrameInfo) async -> Bool { |
| + guard let vc = Self.topViewController() else { return false } |
| + let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) |
| + return await withCheckedContinuation { continuation in |
| + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in continuation.resume(returning: false) }) |
| + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in continuation.resume(returning: true) }) |
| + vc.present(alert, animated: true) |
| + } |
| + } |
| + |
| + func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, |
| + initiatedByFrame frame: WKFrameInfo) async { |
| + guard let vc = Self.topViewController() else { return } |
| + let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) |
| + await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in |
| + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in continuation.resume() }) |
| + vc.present(alert, animated: true) |
| + } |
| + } |
| + |
| + private static func topViewController() -> UIViewController? { |
| + guard let scene = UIApplication.shared.connectedScenes |
| + .compactMap({ $0 as? UIWindowScene }) |
| + .first(where: { $0.activationState == .foregroundActive }), |
| + let root = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController else { return nil } |
| + var top = root |
| + while let presented = top.presentedViewController { top = presented } |
| + return top |
| + } |
| + |
| // Whether the web composer currently has a photo staged. Drives the native send |
| // button so an image-only message can be sent. Kept in sync by the "lchat" handler. |
| @Published var attachmentStaged = false |