From 2ba87e4993647e5a6e28cf4212f06743d51fecf9 Mon Sep 17 00:00:00 2001 From: John Tennant Date: Thu, 27 Aug 2026 18:01:27 -0400 Subject: [PATCH] fix(collections): surface pull requests from channels Signed-off-by: John Tennant --- .../collections/derivedLinks.test.mjs | 46 +++++++++++++++++++ .../src/features/collections/derivedLinks.ts | 13 ++++-- desktop/src/features/collections/hooks.ts | 41 ++++++++--------- .../collections/ui/CollectionDetailScreen.tsx | 15 +++++- desktop/tests/e2e/collections.spec.ts | 15 ++++-- 5 files changed, 100 insertions(+), 30 deletions(-) diff --git a/desktop/src/features/collections/derivedLinks.test.mjs b/desktop/src/features/collections/derivedLinks.test.mjs index e7da4ff931..ba1d6116ee 100644 --- a/desktop/src/features/collections/derivedLinks.test.mjs +++ b/desktop/src/features/collections/derivedLinks.test.mjs @@ -229,6 +229,52 @@ test("suppresses matching PR mention rows across sources but keeps unrelated cha ); }); +test("suppresses a PR conversation without hiding another thread from the same channel source", () => { + const message = (eventId, threadRootId, label) => ({ + activityType: "channel-message", + actorIdentity: "buzz:human", + authorPubkey: "human", + channelId: "channel-1", + createdAt: 1, + eventId, + kind: "thread", + label, + sourceMemberId: "channel-source", + threadRootId, + url: "", + }); + const prConversation = message("pr-reply", "pr-thread", "PR chatter"); + const unrelatedConversation = message( + "design-reply", + "design-thread", + "Unrelated design decision", + ); + const pullRequest = { + ...prConversation, + activityType: "github-pr", + kind: "pull request", + label: "PR #7", + sourceEventIds: ["pr-reply"], + sourceMemberIds: ["channel-source"], + url: "https://github.com/block/buzz/pull/7", + }; + const stateChange = { + ...pullRequest, + activityType: "github-pr-activity", + createdAt: 2, + kind: "PR review", + url: `${pullRequest.url}#pullrequestreview-1`, + }; + + assert.deepEqual( + projectCollectionActivity( + [prConversation, unrelatedConversation, pullRequest, stateChange], + { human: false }, + ), + [unrelatedConversation, stateChange], + ); +}); + test("names channel-derived live thread provenance", () => { assert.equal( collectionChannelThreadProvenance("#design"), diff --git a/desktop/src/features/collections/derivedLinks.ts b/desktop/src/features/collections/derivedLinks.ts index 0f08fde0f7..5cbae89518 100644 --- a/desktop/src/features/collections/derivedLinks.ts +++ b/desktop/src/features/collections/derivedLinks.ts @@ -362,15 +362,22 @@ export function projectCollectionActivity( activity: readonly DerivedCollectionActivity[], actorIsAgent: Readonly>, ): DerivedCollectionActivity[] { + const conversationIdentity = (item: DerivedCollectionActivity) => + item.channelId + ? `${item.channelId}:${item.threadRootId ?? item.eventId ?? item.sourceMemberId}` + : null; const pullRequestUrls = new Set( activity .filter((item) => item.activityType === "github-pr") .map((item) => item.url.toLocaleLowerCase()), ); - const pullRequestSources = new Set( + const pullRequestConversations = new Set( activity .filter((item) => item.activityType === "github-pr") - .flatMap((item) => item.sourceMemberIds ?? [item.sourceMemberId]), + .flatMap((item) => { + const identity = conversationIdentity(item); + return identity ? [identity] : []; + }), ); const pullRequestEvents = new Set( activity @@ -384,7 +391,7 @@ export function projectCollectionActivity( ]).some((mention) => pullRequestUrls.has(mention.url.toLocaleLowerCase())); return ( !mentionsResolvedPullRequest && - !pullRequestSources.has(item.sourceMemberId) && + !pullRequestConversations.has(conversationIdentity(item) ?? "") && (!item.eventId || !pullRequestEvents.has(item.eventId)) ); }); diff --git a/desktop/src/features/collections/hooks.ts b/desktop/src/features/collections/hooks.ts index af1aa63443..6aec009d88 100644 --- a/desktop/src/features/collections/hooks.ts +++ b/desktop/src/features/collections/hooks.ts @@ -67,7 +67,6 @@ function sourceLabel(member: CollectionMember): string { function messageActivity( member: CollectionMember, events: Awaited>, - includePullRequests: boolean, ): DerivedCollectionActivity[] { const reference = member.reference; if ( @@ -102,25 +101,21 @@ function messageActivity( reference.type === "thread" ? reference.root_event_id : thread.rootId, url: "", }; - const pullRequests = includePullRequests - ? extractGitHubPullRequestLinks([event]).map( - (link): DerivedCollectionActivity => ({ - ...link, - activityType: "github-pr", - actorIdentity: `buzz:${event.pubkey.toLocaleLowerCase()}`, - authorPubkey: event.pubkey, - channelId, - createdAt: event.created_at, - eventId: event.id, - provenanceLabel: `${sourceLabel(member)} → mentioned PR`, - sourceMemberId: member.id, - threadRootId: - reference.type === "thread" - ? reference.root_event_id - : thread.rootId, - }), - ) - : []; + const pullRequests = extractGitHubPullRequestLinks([event]).map( + (link): DerivedCollectionActivity => ({ + ...link, + activityType: "github-pr", + actorIdentity: `buzz:${event.pubkey.toLocaleLowerCase()}`, + authorPubkey: event.pubkey, + channelId, + createdAt: event.created_at, + eventId: event.id, + provenanceLabel: `${sourceLabel(member)} → mentioned PR`, + sourceMemberId: member.id, + threadRootId: + reference.type === "thread" ? reference.root_event_id : thread.rootId, + }), + ); return [message, ...pullRequests]; }); } @@ -332,7 +327,7 @@ async function discoverCollectionMemberActivity( }, ); return { - items: [...threadActivity, ...messageActivity(member, messages, false)], + items: [...threadActivity, ...messageActivity(member, messages)], warnings: [], }; } @@ -344,14 +339,14 @@ async function discoverCollectionMemberActivity( { limit: COLLECTION_CHANNEL_DISCOVERY_LIMIT }, ); return { - items: messageActivity(member, [root, ...replies.events], true), + items: messageActivity(member, [root, ...replies.events]), warnings: [], }; } if (member.reference.type === "message") { const event = await getEventById(member.reference.event_id); return { - items: messageActivity(member, [event], true), + items: messageActivity(member, [event]), warnings: [], }; } diff --git a/desktop/src/features/collections/ui/CollectionDetailScreen.tsx b/desktop/src/features/collections/ui/CollectionDetailScreen.tsx index c5af1f4229..60f356be47 100644 --- a/desktop/src/features/collections/ui/CollectionDetailScreen.tsx +++ b/desktop/src/features/collections/ui/CollectionDetailScreen.tsx @@ -52,6 +52,8 @@ import { CollectionRowActions, DerivedCollectionActivityRow, } from "./CollectionDetailRows"; + +const PEOPLE_PREVIEW_LIMIT = 8; import { CollectionGlyph } from "./CollectionGlyph"; import { CollectionIconDialog } from "./CollectionIconDialog"; @@ -98,6 +100,12 @@ export function CollectionDetailScreen({ !identity.startsWith("buzz:") || !profiles[identity.slice("buzz:".length)]?.isAgent, ); + const visiblePeopleIdentities = peopleIdentities.slice( + 0, + PEOPLE_PREVIEW_LIMIT, + ); + const hiddenPeopleCount = + peopleIdentities.length - visiblePeopleIdentities.length; const members = (data?.members ?? []).filter((member) => collectionMemberMatches(member, search, type), ); @@ -454,7 +462,7 @@ export function CollectionDetailScreen({

- {peopleIdentities.map((identity) => { + {visiblePeopleIdentities.map((identity) => { const pubkey = identity.startsWith("buzz:") ? identity.slice("buzz:".length) : null; @@ -494,6 +502,11 @@ export function CollectionDetailScreen({ ); })} + {hiddenPeopleCount > 0 ? ( + + +{hiddenPeopleCount} more + + ) : null} {selectedPerson ? (