From 58696ff577c96ff40e2cb155cf8595d03262c307 Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Mon, 9 Nov 2026 06:59:13 -0800 Subject: [PATCH] fix(mobile): show the channel name as the notification subtitle (#9173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Apple Watch, a Buzz notification showed the community name ("buzz") under the sender, taking a line of the small display without saying where the message came from. iPhone never showed it, because iOS draws the banner from the communication intent and drops `content.subtitle`. The Watch renders the plain fields. The subtitle now carries the conversation name, the same one the intent uses for its group name: - Team channels and forums: `#channel` - Your own Solo chats: the bare name, no `#` - DMs, and channels not yet in the presentation cache: no subtitle The name logic moves into one helper so the subtitle and the iPhone group name can't drift apart. A channel that's known but isn't eligible for the communication style (for example, an unverified roster) now gets `#channel` as its plain-banner subtitle in place of the community name. Losing the community name means the Watch no longer tells communities apart. That seems like the right trade for most people, who are in one community. ### Validation - BuzzPushKit `swift test`: all 110 XCTest and 69 Swift Testing cases pass. Updated the resolver assertions: `#General` for a cached stream, both with and without communication styling, and no subtitle for DMs and uncached channels. - Not yet exercised on a device or Watch. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tom Brow Co-authored-by: Tom Brow Co-authored-by: Claude Opus 5.5 --- .../BuzzPushNotificationResolver.swift | 20 ++++++++++++++++--- .../BuzzPushConversationResolverTests.swift | 5 +++-- .../BuzzPushNotificationResolverTests.swift | 6 +++--- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNotificationResolver.swift b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNotificationResolver.swift index aec145222e..da32635ebf 100644 --- a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNotificationResolver.swift +++ b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNotificationResolver.swift @@ -533,7 +533,9 @@ public final class BuzzPushNotificationResolver: BuzzPushNotificationResolving { return BuzzPushResolution( title: profile?.displayName ?? "", body: body, - subtitle: community.name, + subtitle: channel.flatMap { + channelDisplayName(channel: $0, currentUser: community.pubkey?.lowercased()) + }, threadIdentifier: conversationIdentifier ?? community.id, navigationTarget: channelID.map { BuzzPushNavigationTarget( @@ -635,6 +637,18 @@ public final class BuzzPushNotificationResolver: BuzzPushNotificationResolving { guard memberCount == 2, senderIsMember else { return nil } return (nil, recipientCount) } + guard let displayName = channelDisplayName(channel: channel, currentUser: currentUser) + else { return nil } + return (displayName, recipientCount) + } + + /// The conversation name shown with a channel message: `#name` for team + /// channels and forums, the bare name for your own Solo chats, and `nil` + /// for DMs and unrecognized channel types. + private static func channelDisplayName( + channel: BuzzPushCachedChannel, + currentUser: String? + ) -> String? { guard let channelType = channel.channelType, ["stream", "forum"].contains(channelType), let displayName = channel.displayName @@ -643,9 +657,9 @@ public final class BuzzPushNotificationResolver: BuzzPushNotificationResolving { // get no `#`. Like the app (`Channel.isPersonalChat`), someone else's // Solo chat you belong to stays an ordinary channel. if channel.isSolo == true, channel.creatorPubkey == currentUser { - return (displayName, recipientCount) + return displayName } - return (displayName.hasPrefix("#") ? displayName : "#\(displayName)", recipientCount) + return displayName.hasPrefix("#") ? displayName : "#\(displayName)" } private static func newest(_ events: [VerifiedNostrEvent]) -> VerifiedNostrEvent? { diff --git a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushConversationResolverTests.swift b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushConversationResolverTests.swift index 9d0f817d41..e5492d4e07 100644 --- a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushConversationResolverTests.swift +++ b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushConversationResolverTests.swift @@ -48,6 +48,7 @@ extension BuzzPushNotificationResolverTests { ) XCTAssertEqual(result.conversationDisplayName, "#General") + XCTAssertEqual(result.subtitle, "#General") XCTAssertEqual(result.conversationRecipientCount, 2) } @@ -96,7 +97,7 @@ extension BuzzPushNotificationResolverTests { XCTAssertNil(result.conversationDisplayName) XCTAssertNil(result.conversationRecipientCount) - XCTAssertEqual(result.subtitle, "Community") + XCTAssertEqual(result.subtitle, "#General") } func testPartialMembershipRefreshDoesNotReuseCachedChannel() throws { @@ -262,6 +263,6 @@ extension BuzzPushNotificationResolverTests { XCTAssertNil(result.conversationDisplayName) XCTAssertNil(result.conversationRecipientCount) - XCTAssertEqual(result.subtitle, "Community") + XCTAssertNil(result.subtitle) } } diff --git a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNotificationResolverTests.swift b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNotificationResolverTests.swift index 339941b672..5682335960 100644 --- a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNotificationResolverTests.swift +++ b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNotificationResolverTests.swift @@ -311,7 +311,7 @@ final class BuzzPushNotificationResolverTests: XCTestCase { XCTAssertEqual(result?.0.title, sender.title, "pubkey: \(sender.pubkey)") XCTAssertEqual(result?.0.body, "Preview", "pubkey: \(sender.pubkey)") - XCTAssertEqual(result?.0.subtitle, "Community", "pubkey: \(sender.pubkey)") + XCTAssertNil(result?.0.subtitle, "pubkey: \(sender.pubkey)") XCTAssertEqual(result?.0.senderPubkey, sender.pubkey, "pubkey: \(sender.pubkey)") XCTAssertEqual(result?.0.threadIdentifier, "community-id", "pubkey: \(sender.pubkey)") } @@ -367,7 +367,7 @@ final class BuzzPushNotificationResolverTests: XCTestCase { XCTAssertNotEqual(result.body, Self.gatewayBody) XCTAssertEqual(result.title, "") XCTAssertEqual(result.body, "Hello Buzz") - XCTAssertEqual(result.subtitle, "Community") + XCTAssertNil(result.subtitle) XCTAssertEqual( result.threadIdentifier, BuzzPushPresentationIdentity.conversation( @@ -1024,7 +1024,7 @@ final class BuzzPushNotificationResolverTests: XCTestCase { XCTAssertEqual(result.title, "") XCTAssertNil(result.conversationDisplayName) - XCTAssertEqual(result.subtitle, "Community") + XCTAssertNil(result.subtitle) XCTAssertEqual(result.body, "Fallback content") }