| index 367695a..e53fb85 100644 |
| --- a/ios/LChatMac/NativeChatView.swift |
| +++ b/ios/LChatMac/NativeChatView.swift |
| @@ -1479,47 +1479,78 @@ struct NativeChatView: View { |
| private func notifyIfBackgrounded(_ frame: ChatSocket.Frame) { |
| guard !NSApp.isActive, |
| let tid = frame.thread_id, let blob = frame.blob, |
| - let thread = store.threads.first(where: { $0.id == tid }) |
| + let thread = store.threads.first(where: { $0.id == tid }), |
| + !thread.isMuted |
| else { return } |
| let myUserId = store.myUserId |
| let instanceID = instance.id |
| let origin = instance.origin |
| + let labels = store.senderLabels |
| + let mutedPeople = store.mutedPeople |
| // The AES open runs detached — this fires for every message on the |
| // instance socket while backgrounded; posting the banner is |
| // thread-safe. |
| Task.detached(priority: .utility) { |
| guard let key = try? GroupKey(base64url: thread.threadKeyB64), |
| let p = try? key.decryptJSON(MessagePayload.self, blob: blob), |
| - p.uid != myUserId |
| + p.uid != myUserId, |
| + // A muted person never buzzes (#578) — their uid rides inside |
| + // the ciphertext, so only this post-decrypt point can know. |
| + // Local banners can drop outright where the iOS extension |
| + // must ship a passive entry. |
| + !(p.uid.map { mutedPeople.contains($0) } ?? false) |
| else { return } |
| + // Senders resolve like the iOS extension: the name THIS member |
| + // shows for them (per-viewer names, #577), then the payload's |
| + // snapshot. nil = no real person to show (app/system events). |
| + let resolvedName: String? = { |
| + if let uid = p.uid, let named = labels[uid], !named.isEmpty { return named } |
| + if let n = p.name, !n.isEmpty { return n } |
| + return nil |
| + }() |
| + let sender = resolvedName ?? "Someone" |
| let content = UNMutableNotificationContent() |
| content.title = thread.title |
| + content.threadIdentifier = tid |
| if p.isReactionEvent { |
| // Reactions notify only the reacted-to message's author — and |
| // the Mac posts local notifications, so unlike the iOS NSE it |
| // can drop the rest outright. Un-reacts and reactions to |
| // legacy uid-less messages notify no one. |
| guard p.on == true, let target = p.targetUid, target == myUserId else { return } |
| - let sender = p.name ?? "Someone" |
| let emoji = p.emoji ?? "" |
| content.body = thread.kind == "group" |
| ? "\(sender) reacted \(emoji) to your message" |
| : "Reacted \(emoji) to your message" |
| - } else { |
| - content.body = previewText(kind: thread.kind, payload: p) |
| + content.sound = .default |
| + content.userInfo = ["instance": instanceID, "thread": tid] |
| + try? await UNUserNotificationCenter.current().add( |
| + UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)) |
| + return |
| + } |
| + // The banner text mirrors the iOS extension exactly — and it must |
| + // never be empty: macOS re-lays-out a communication banner with no |
| + // body as "app name + sender", dropping the message entirely (a |
| + // textless app event rendered as the baffling "Eleven / James"). |
| + var text = markdownPlainText(p.text ?? "") |
| + if text.isEmpty { |
| + let n = p.attachments?.count ?? 0 |
| + text = n > 0 ? (n == 1 ? "📎 Attachment" : "📎 \(n) attachments") : "New message" |
| } |
| + // In a DM the title already names the sender; groups prefix them. |
| + content.body = thread.kind == "group" ? "\(sender): \(text)" : text |
| content.sound = .default |
| content.userInfo = ["instance": instanceID, "thread": tid] |
| // Sender's face on the banner where possible — same restyle the |
| // iOS extension applies (CommunicationNotification); a build |
| // without the communication entitlement just keeps the banner. |
| var deliver: UNNotificationContent = content |
| - if let name = p.name, !name.isEmpty, |
| + if let name = resolvedName, |
| let styled = CommunicationNotification.decorate( |
| content, senderName: name, senderUID: p.uid, |
| origin: origin, conversationID: tid, |
| groupName: thread.kind == "group" ? thread.title : nil, |
| - messageText: previewText(kind: "dm", payload: p)) { |
| + messageText: text) { |
| deliver = styled |
| } |
| try? await UNUserNotificationCenter.current().add( |