Skip to content

Commit 19220f7

Browse files
committed
add e tags to deletes to be safe
1 parent 8ab46ed commit 19220f7

4 files changed

Lines changed: 124 additions & 11 deletions

File tree

src/components/profile/CommonGroupsListImproved.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useAuthor } from "@/hooks/useAuthor";
1313
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
1414
import { Separator } from "@/components/ui/separator";
1515
import { KINDS } from "@/lib/nostr-kinds";
16+
import { useGroupDeletionRequests } from "@/hooks/useGroupDeletionRequests";
1617

1718
interface CommonGroup {
1819
id: string;
@@ -263,6 +264,17 @@ export function CommonGroupsListImproved({ profileUserPubkey }: CommonGroupsList
263264
enabled: !!nostr && !!user && !!profileUserPubkey && user.pubkey !== profileUserPubkey,
264265
});
265266

267+
// Get group IDs for deletion checking
268+
const groupIds = commonGroups?.map(group => group.id) || [];
269+
const { data: deletionRequests } = useGroupDeletionRequests(groupIds);
270+
271+
// Filter out deleted groups
272+
const filteredCommonGroups = commonGroups?.filter(group => {
273+
if (!deletionRequests) return true;
274+
const deletionRequest = deletionRequests.get(group.id);
275+
return !(deletionRequest?.isValid || false);
276+
}) || [];
277+
266278
// Don't show anything if viewing own profile
267279
if (!user || !profileUserPubkey || user.pubkey === profileUserPubkey) {
268280
return null;
@@ -292,7 +304,7 @@ export function CommonGroupsListImproved({ profileUserPubkey }: CommonGroupsList
292304
);
293305
}
294306

295-
if (!commonGroups || commonGroups.length === 0) {
307+
if (!filteredCommonGroups || filteredCommonGroups.length === 0) {
296308
return null; // Don't show section if no common groups
297309
}
298310

@@ -304,12 +316,12 @@ export function CommonGroupsListImproved({ profileUserPubkey }: CommonGroupsList
304316
<h3 className="text-lg font-semibold">Shared Groups</h3>
305317
</div>
306318
<Badge variant="secondary" className="text-xs font-medium">
307-
{commonGroups.length} {commonGroups.length === 1 ? 'group' : 'groups'}
319+
{filteredCommonGroups.length} {filteredCommonGroups.length === 1 ? 'group' : 'groups'}
308320
</Badge>
309321
</div>
310322

311323
<div className="space-y-3">
312-
{commonGroups.map((group) => (
324+
{filteredCommonGroups.map((group) => (
313325
<Link
314326
key={group.id}
315327
to={`/group/${encodeURIComponent(group.id)}`}

src/hooks/useGroupDeletionRequests.ts

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,47 @@ export function useGroupDeletionRequests(groupIds: string[]) {
2727
try {
2828
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]);
2929

30-
// Query for kind 5 deletion events that reference groups via 'a' tags
31-
const deletionEvents = await nostr.query([{
32-
kinds: [KINDS.DELETION],
33-
"#a": groupIds,
34-
limit: 1000 // Should be enough for deletion requests
35-
}], { signal });
30+
// Query for kind 5 deletion events that reference groups via 'a' tags or 'e' tags
31+
// We need to extract event IDs from group IDs for 'e' tag filtering
32+
const groupEventIds: string[] = [];
33+
34+
// For each group ID, we need to query for the actual group event to get its event ID
35+
// This is needed for 'e' tag compatibility
36+
for (const groupId of groupIds) {
37+
const parts = groupId.split(":");
38+
if (parts.length === 3 && parts[0] === KINDS.GROUP.toString()) {
39+
const [, pubkey, identifier] = parts;
40+
try {
41+
const groupEvents = await nostr.query([{
42+
kinds: [KINDS.GROUP],
43+
authors: [pubkey],
44+
"#d": [identifier],
45+
limit: 1
46+
}], { signal: AbortSignal.timeout(2000) });
47+
48+
if (groupEvents.length > 0) {
49+
groupEventIds.push(groupEvents[0].id);
50+
}
51+
} catch (error) {
52+
// If we can't fetch the group event, skip it for 'e' tag filtering
53+
console.warn("Could not fetch group event for deletion filtering:", groupId, error);
54+
}
55+
}
56+
}
57+
58+
// Query for deletion events using both 'a' tags and 'e' tags
59+
const deletionEvents = await nostr.query([
60+
{
61+
kinds: [KINDS.DELETION],
62+
"#a": groupIds,
63+
limit: 500
64+
},
65+
...(groupEventIds.length > 0 ? [{
66+
kinds: [KINDS.DELETION],
67+
"#e": groupEventIds,
68+
limit: 500
69+
}] : [])
70+
], { signal });
3671

3772
const deletionMap = new Map<string, GroupDeletionRequest>();
3873

@@ -43,6 +78,7 @@ export function useGroupDeletionRequests(groupIds: string[]) {
4378
tag[0] === "a" && tag[1] && tag[1].startsWith(`${KINDS.GROUP}:`)
4479
);
4580

81+
// Process 'a' tags (addressable event references)
4682
for (const aTag of aTags) {
4783
const groupId = aTag[1];
4884

@@ -68,6 +104,57 @@ export function useGroupDeletionRequests(groupIds: string[]) {
68104
});
69105
}
70106
}
107+
108+
// Also check 'e' tags for event ID references
109+
// This provides compatibility with relays that primarily use 'e' tags for deletions
110+
const eTags = deletionEvent.tags.filter(tag => tag[0] === "e" && tag[1]);
111+
112+
if (eTags.length > 0) {
113+
// For 'e' tag processing, we need to be more careful since we need to match
114+
// event IDs to group IDs. We'll query for the referenced events to validate them.
115+
for (const eTag of eTags) {
116+
const eventId = eTag[1];
117+
118+
try {
119+
// Query for the referenced event to see if it's a group event
120+
const referencedEvents = await nostr.query([{
121+
ids: [eventId],
122+
kinds: [KINDS.GROUP],
123+
limit: 1
124+
}], { signal: AbortSignal.timeout(2000) });
125+
126+
if (referencedEvents.length > 0) {
127+
const groupEvent = referencedEvents[0];
128+
const dTag = groupEvent.tags.find(tag => tag[0] === "d");
129+
130+
if (dTag) {
131+
const groupId = `${KINDS.GROUP}:${groupEvent.pubkey}:${dTag[1]}`;
132+
133+
// Check if this group is in our target list
134+
if (groupIds.includes(groupId)) {
135+
// Validate that the deletion request is from the group owner
136+
const isValid = deletionEvent.pubkey === groupEvent.pubkey;
137+
138+
// Only store the most recent valid deletion request for each group
139+
const existing = deletionMap.get(groupId);
140+
if (!existing || (isValid && deletionEvent.created_at > existing.deletionEvent.created_at)) {
141+
deletionMap.set(groupId, {
142+
deletionEvent,
143+
groupId,
144+
isValid,
145+
reason: deletionEvent.content || undefined
146+
});
147+
}
148+
}
149+
}
150+
}
151+
} catch (error) {
152+
// If we can't fetch the referenced event, skip this 'e' tag
153+
console.warn("Could not fetch referenced event for deletion validation:", eventId, error);
154+
continue;
155+
}
156+
}
157+
}
71158
}
72159

73160
return deletionMap;

src/pages/GroupDetail.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,10 +387,12 @@ export default function GroupDetail() {
387387

388388
try {
389389
// Create a kind 5 deletion event referencing the group
390+
// Include both 'a' tag (addressable event) and 'e' tag (event ID) for maximum relay compatibility
390391
await publishEvent({
391392
kind: KINDS.DELETION,
392393
tags: [
393394
["a", `${KINDS.GROUP}:${community.pubkey}:${parsedId.identifier}`],
395+
["e", community.id],
394396
["k", KINDS.GROUP.toString()]
395397
],
396398
content: "Requesting deletion of this group",

src/pages/Profile.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import {
6363
} from "@/components/ui/dropdown-menu";
6464
import { QRCodeModal } from "@/components/QRCodeModal";
6565
import { CommonGroupsListImproved } from "@/components/profile/CommonGroupsListImproved";
66-
import { useIsGroupDeleted } from "@/hooks/useGroupDeletionRequests";
66+
import { useIsGroupDeleted, useGroupDeletionRequests } from "@/hooks/useGroupDeletionRequests";
6767

6868
// Hook to get shared group IDs
6969
function useSharedGroupIds(profileUserPubkey: string): string[] {
@@ -428,6 +428,10 @@ function UserGroupsList({
428428
const profileMetadata = profileAuthor.data?.metadata;
429429
const profileDisplayName = profileMetadata?.name || profileUserPubkey.slice(0, 8);
430430

431+
// Get group IDs for deletion checking
432+
const groupIds = groups?.map(group => group.id) || [];
433+
const { data: deletionRequests } = useGroupDeletionRequests(groupIds);
434+
431435
if (isLoading) {
432436
return (
433437
<div className="space-y-4">
@@ -460,14 +464,22 @@ function UserGroupsList({
460464
);
461465
}
462466

463-
// Create a map to deduplicate groups by ID and filter out shared groups
467+
// Create a map to deduplicate groups by ID and filter out shared groups and deleted groups
464468
const uniqueGroups = new Map<string, UserGroup>();
465469
for (const group of groups) {
466470
// Skip if this group is in the shared groups list (only when viewing another user's profile)
467471
if (!isCurrentUser && sharedGroupIds.includes(group.id)) {
468472
continue;
469473
}
470474

475+
// Skip if this group has been deleted
476+
if (deletionRequests) {
477+
const deletionRequest = deletionRequests.get(group.id);
478+
if (deletionRequest?.isValid) {
479+
continue;
480+
}
481+
}
482+
471483
// Only add if not already in the map, or replace with newer version
472484
if (
473485
!uniqueGroups.has(group.id) ||

0 commit comments

Comments
 (0)