2026-07-28 16:16:26 +00:00
|
|
|
import {derived} from "svelte/store"
|
|
|
|
|
import {first, groupBy, removeUndefined, spec} from "@welshman/lib"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {
|
|
|
|
|
REPORT,
|
|
|
|
|
ROOM_ADD_MEMBER,
|
|
|
|
|
ROOM_JOIN,
|
|
|
|
|
ROOM_LEAVE,
|
|
|
|
|
ROOM_MEMBERS,
|
|
|
|
|
ROOM_REMOVE_MEMBER,
|
2026-07-28 16:16:26 +00:00
|
|
|
hexTags,
|
|
|
|
|
sortEventsAsc,
|
2026-06-08 22:33:38 +00:00
|
|
|
sortEventsDesc,
|
2026-07-28 16:16:26 +00:00
|
|
|
tagSpec,
|
|
|
|
|
tagValue,
|
|
|
|
|
tagValues,
|
2026-06-08 22:33:38 +00:00
|
|
|
} from "@welshman/util"
|
|
|
|
|
import type {TrustedEvent} from "@welshman/util"
|
|
|
|
|
import {deriveEventsForUrl} from "@app/repository"
|
2026-07-28 16:16:26 +00:00
|
|
|
|
|
|
|
|
// A room's membership replayed in order — each member list is a snapshot that resets
|
|
|
|
|
// the set, with add/remove events applied on top.
|
|
|
|
|
const getRoomMembers = (events: TrustedEvent[]) => {
|
|
|
|
|
const members = new Set<string>()
|
|
|
|
|
|
|
|
|
|
for (const event of sortEventsAsc(events)) {
|
|
|
|
|
const pubkeys = tagValues(hexTags("p"), event.tags)
|
|
|
|
|
|
|
|
|
|
if (event.kind === ROOM_MEMBERS) {
|
|
|
|
|
members.clear()
|
|
|
|
|
pubkeys.forEach(pubkey => members.add(pubkey))
|
|
|
|
|
} else if (event.kind === ROOM_ADD_MEMBER) {
|
|
|
|
|
pubkeys.forEach(pubkey => members.add(pubkey))
|
|
|
|
|
} else if (event.kind === ROOM_REMOVE_MEMBER) {
|
|
|
|
|
pubkeys.forEach(pubkey => members.delete(pubkey))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return members
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 22:33:38 +00:00
|
|
|
// Action items (admin review queue)
|
|
|
|
|
|
|
|
|
|
export const deriveSpaceActionItems = (url: string) =>
|
|
|
|
|
derived(
|
|
|
|
|
deriveEventsForUrl(url, [
|
2026-07-28 16:16:26 +00:00
|
|
|
{kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER]},
|
2026-06-08 22:33:38 +00:00
|
|
|
]),
|
|
|
|
|
$events => {
|
|
|
|
|
const getRoomId = (e: TrustedEvent) =>
|
2026-07-28 16:16:26 +00:00
|
|
|
tagValue(tagSpec(e.kind === ROOM_MEMBERS ? "d" : "h"), e.tags)
|
|
|
|
|
const reports = $events.filter(spec({kind: REPORT}))
|
2026-06-08 22:33:38 +00:00
|
|
|
const pendingJoins: TrustedEvent[] = []
|
|
|
|
|
|
|
|
|
|
// Room-level join requests — most recent per pubkey+h
|
|
|
|
|
for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
|
|
|
|
|
if (!h) continue
|
|
|
|
|
|
|
|
|
|
const roomJoins: TrustedEvent[] = []
|
|
|
|
|
const roomLeaves: TrustedEvent[] = []
|
|
|
|
|
const roomMembershipEvents: TrustedEvent[] = []
|
|
|
|
|
|
|
|
|
|
for (const event of roomEvents) {
|
|
|
|
|
switch (event.kind) {
|
|
|
|
|
case ROOM_JOIN:
|
|
|
|
|
roomJoins.push(event)
|
|
|
|
|
break
|
|
|
|
|
case ROOM_LEAVE:
|
|
|
|
|
roomLeaves.push(event)
|
|
|
|
|
break
|
|
|
|
|
case ROOM_MEMBERS:
|
|
|
|
|
case ROOM_ADD_MEMBER:
|
|
|
|
|
case ROOM_REMOVE_MEMBER:
|
|
|
|
|
roomMembershipEvents.push(event)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const roomMembers = getRoomMembers(roomMembershipEvents)
|
2026-06-08 22:33:38 +00:00
|
|
|
|
|
|
|
|
pendingJoins.push(
|
|
|
|
|
...removeUndefined(
|
|
|
|
|
Array.from(groupBy(e => e.pubkey, roomJoins).values()).map(events =>
|
|
|
|
|
first(sortEventsDesc(events)),
|
|
|
|
|
),
|
|
|
|
|
).filter(({pubkey, created_at}) => {
|
|
|
|
|
if (roomMembers.has(pubkey)) return false
|
|
|
|
|
if (
|
|
|
|
|
roomMembershipEvents.some(event => {
|
|
|
|
|
if (event.created_at <= created_at) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === ROOM_MEMBERS) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
return tagValues(hexTags("p"), event.tags).includes(pubkey)
|
2026-06-08 22:33:38 +00:00
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at)) return false
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sortEventsDesc([...reports, ...pendingJoins])
|
|
|
|
|
},
|
|
|
|
|
)
|