| index 6948252..78b7439 100644 |
| --- a/ios/LChat/Views/ChatView.swift |
| +++ b/ios/LChat/Views/ChatView.swift |
| @@ -158,6 +158,10 @@ struct ChatView: View { |
| // reply (swipe-right in the feed) shows as a glass banner above the row. |
| private var composer: some View { |
| VStack(spacing: 8) { |
| + if bridge.editing != nil { |
| + editBanner() |
| + .transition(.move(edge: .bottom).combined(with: .opacity)) |
| + } |
| if let replying = bridge.replying { |
| replyBanner(replying) |
| .transition(.move(edge: .bottom).combined(with: .opacity)) |
| @@ -169,6 +173,7 @@ struct ChatView: View { |
| composerRow |
| } |
| .animation(.easeOut(duration: 0.18), value: bridge.replying) |
| + .animation(.easeOut(duration: 0.18), value: bridge.editing) |
| .animation(.easeOut(duration: 0.12), value: bridge.typeahead) |
| // The cluster's blank padding consumes taps: a near-miss focuses the |
| // input rather than falling through to the feed and closing the keyboard. |
| @@ -177,6 +182,17 @@ struct ChatView: View { |
| .onChange(of: bridge.replying) { _, r in |
| if r != nil { composerFocused = true } // mirror the web: replying focuses the input |
| } |
| + // Edit mode is driven by the web layer (message menu → Edit). Prefill the |
| + // native composer with the original text so it's visible + editable; clear |
| + // it when edit ends (sent or cancelled, both arrive as editing == nil). |
| + .onChange(of: bridge.editing) { _, e in |
| + if let e { |
| + text = e.text |
| + composerFocused = true |
| + } else { |
| + text = "" |
| + } |
| + } |
| .onChange(of: text) { _, _ in |
| bridge.updateTypeahead(text: text, caretUTF16: caret) |
| bridge.noteTyping(hasText: !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) |
| @@ -218,6 +234,32 @@ struct ChatView: View { |
| composerFocused = true |
| } |
| |
| + // Mirrors replyBanner: a glass "Editing message" strip above the composer while |
| + // an edit is in flight. The ✕ drops edit mode on both sides (bridge.cancelEdit). |
| + private func editBanner() -> some View { |
| + HStack(spacing: 10) { |
| + Image(systemName: "pencil") |
| + .font(.footnote) |
| + .foregroundStyle(Color.accentColor) |
| + Text("Editing message") |
| + .font(.footnote.weight(.semibold)) |
| + .lineLimit(1) |
| + Spacer(minLength: 0) |
| + Button { bridge.cancelEdit() } label: { |
| + Image(systemName: "xmark.circle.fill") |
| + .font(.system(size: 18)) |
| + .foregroundStyle(Color(.tertiaryLabel)) |
| + } |
| + .buttonStyle(.plain) |
| + .accessibilityLabel("Cancel edit") |
| + } |
| + .padding(.horizontal, 12) |
| + .padding(.vertical, 8) |
| + .floatingGlass(in: RoundedRectangle(cornerRadius: 16)) |
| + .padding(.horizontal, 10) |
| + .padding(.top, 8) |
| + } |
| + |
| private func replyBanner(_ replying: WebBridge.Replying) -> some View { |
| HStack(spacing: 10) { |
| Image(systemName: "arrowshape.turn.up.left.fill") |
| @@ -413,6 +455,20 @@ final class WebBridge: NSObject, ObservableObject, WKScriptMessageHandler, WKNav |
| "window.lchatNative && window.lchatNative.cancelReply && window.lchatNative.cancelReply()") |
| } |
| |
| + // The edit the web layer entered (message menu → Edit). In embed mode the web |
| + // composer + edit banner are hidden, so the native shell prefills its composer |
| + // with the original text and shows its own "Editing" banner. A native send then |
| + // hits chat.js's editing path; on success/cancel chat.js posts {editing:null}. |
| + struct Editing: Equatable { let text: String } |
| + @Published var editing: Editing? |
| + |
| + // The native edit banner's ✕: clear locally, then tell chat.js to drop edit mode. |
| + func cancelEdit() { |
| + editing = nil |
| + webView?.evaluateJavaScript( |
| + "window.lchatNative && window.lchatNative.cancelEdit && window.lchatNative.cancelEdit()") |
| + } |
| + |
| // Composer typeahead (:emoji / @mention): token detection and ranking live |
| // in the web layer (one implementation for all clients); native just renders |
| // the strip and splices the pick into its field. Offsets are UTF-16 on both |
| @@ -512,6 +568,13 @@ final class WebBridge: NSObject, ObservableObject, WKScriptMessageHandler, WKNav |
| replying = nil // {replying: null} — reply cancelled or sent |
| } |
| } |
| + if body.keys.contains("editing") { |
| + if let e = body["editing"] as? [String: Any] { |
| + editing = Editing(text: (e["text"] as? String) ?? "") |
| + } else { |
| + editing = nil // {editing: null} — edit cancelled or sent |
| + } |
| + } |
| } |
| } |
| |