From a730384baf05c65bfe966ac235e10f26e09edeee Mon Sep 17 00:00:00 2001
From: Matthew Remmel
Date: Wed, 15 Oct 2025 12:04:17 -0400
Subject: [PATCH] Add relay members list and room join/leave events
---
src/app/components/AlertAdd.svelte | 4 +-
.../components/ChannelItemAddMember.svelte | 18 +
.../components/ChannelItemRemoveMember.svelte | 18 +
src/app/components/MenuSpace.svelte | 15 +-
src/app/components/MenuSpaces.svelte | 6 +-
src/app/components/PrimaryNav.svelte | 10 +-
src/app/components/ProfileBadges.svelte | 18 +-
src/app/components/ProfileDelete.svelte | 14 +-
src/app/components/ProfileSpaces.svelte | 5 +-
src/app/components/RelaySummary.svelte | 11 +-
src/app/components/SpaceInvite.svelte | 6 +-
src/app/core/commands.ts | 32 +-
src/app/core/requests.ts | 8 +-
src/app/core/state.ts | 481 +++++++++++-------
src/app/core/sync.ts | 38 +-
src/app/util/notifications.ts | 20 +-
src/app/util/routes.ts | 5 +-
src/routes/discover/+page.svelte | 21 +-
src/routes/people/+page.svelte | 4 +-
src/routes/spaces/[relay]/[room]/+page.svelte | 48 +-
src/routes/spaces/[relay]/chat/+page.svelte | 36 +-
tailwind.config.js | 4 +
22 files changed, 499 insertions(+), 323 deletions(-)
create mode 100644 src/app/components/ChannelItemAddMember.svelte
create mode 100644 src/app/components/ChannelItemRemoveMember.svelte
diff --git a/src/app/components/AlertAdd.svelte b/src/app/components/AlertAdd.svelte
index 51717ca..b8f9210 100644
--- a/src/app/components/AlertAdd.svelte
+++ b/src/app/components/AlertAdd.svelte
@@ -13,7 +13,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
- import {alerts, getMembershipUrls, userMembership} from "@app/core/state"
+ import {alerts, userSpaceUrls} from "@app/core/state"
import {requestRelayClaim} from "@app/core/requests"
import {createAlert} from "@app/core/commands"
import {canSendPushNotifications} from "@app/util/push"
@@ -174,7 +174,7 @@
{#snippet input()}
diff --git a/src/app/components/ChannelItemAddMember.svelte b/src/app/components/ChannelItemAddMember.svelte
new file mode 100644
index 0000000..8fe4dd6
--- /dev/null
+++ b/src/app/components/ChannelItemAddMember.svelte
@@ -0,0 +1,18 @@
+
+
+{#each getPubkeyTagValues(event.tags) as pubkey}
+
+{/each}
diff --git a/src/app/components/ChannelItemRemoveMember.svelte b/src/app/components/ChannelItemRemoveMember.svelte
new file mode 100644
index 0000000..bf2c3b3
--- /dev/null
+++ b/src/app/components/ChannelItemRemoveMember.svelte
@@ -0,0 +1,18 @@
+
+
+{#each getPubkeyTagValues(event.tags) as pubkey}
+
+{/each}
diff --git a/src/app/components/MenuSpace.svelte b/src/app/components/MenuSpace.svelte
index eb79053..ab11e2e 100644
--- a/src/app/components/MenuSpace.svelte
+++ b/src/app/components/MenuSpace.svelte
@@ -39,9 +39,7 @@
import {
ENABLE_ZAPS,
MESSAGE_FILTER,
- userRoomsByUrl,
- hasMembershipUrl,
- memberships,
+ deriveSpaceMembers,
deriveEventsForUrl,
deriveUserRooms,
deriveOtherRooms,
@@ -62,6 +60,7 @@
const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
+ const members = deriveSpaceMembers(url)
const owner = $derived($relay?.profile?.pubkey)
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
@@ -83,7 +82,7 @@
const showMembers = () =>
pushModal(
ProfileList,
- {url, pubkeys: members, title: `Members of`, subtitle: displayRelayUrl(url)},
+ {url, pubkeys: $members, title: `Members of`, subtitle: displayRelayUrl(url)},
{replaceState},
)
@@ -108,10 +107,6 @@
let replaceState = $state(false)
let element: Element | undefined = $state()
- const members = $derived(
- $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey),
- )
-
onMount(() => {
replaceState = Boolean(element?.closest(".drawer"))
})
@@ -151,7 +146,7 @@
{#if owner}
@@ -163,7 +158,7 @@
{/if}
- {#if $userRoomsByUrl.has(url)}
+ {#if $userRooms.includes(url)}
{/snippet}
diff --git a/src/app/core/commands.ts b/src/app/core/commands.ts
index 12ec2c6..35c1296 100644
--- a/src/app/core/commands.ts
+++ b/src/app/core/commands.ts
@@ -35,7 +35,7 @@ import {
RELAYS,
FOLLOWS,
REACTION,
- AUTH_JOIN,
+ RELAY_JOIN,
ROOMS,
COMMENT,
ALERT_EMAIL,
@@ -97,15 +97,14 @@ import type {SettingsValues, Alert} from "@app/core/state"
import {
SETTINGS,
PROTECTED,
- userMembership,
INDEXER_RELAYS,
NOTIFIER_PUBKEY,
NOTIFIER_RELAY,
DEFAULT_BLOSSOM_SERVERS,
- userRoomsByUrl,
+ userSpaceUrls,
userSettingsValues,
userInboxRelays,
- getMembershipUrls,
+ userGroupSelections,
} from "@app/core/state"
import {loadAlertStatuses} from "@app/core/requests"
import {platform, platformName, getPushInfo} from "@app/util/push"
@@ -175,7 +174,7 @@ export const broadcastUserData = async (relays: string[]) => {
// List updates
export const addSpaceMembership = async (url: string) => {
- const list = get(userMembership) || makeList({kind: ROOMS})
+ const list = get(userGroupSelections) || makeList({kind: ROOMS})
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
@@ -183,7 +182,7 @@ export const addSpaceMembership = async (url: string) => {
}
export const removeSpaceMembership = async (url: string) => {
- const list = get(userMembership) || makeList({kind: ROOMS})
+ const list = get(userGroupSelections) || makeList({kind: ROOMS})
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
@@ -192,7 +191,7 @@ export const removeSpaceMembership = async (url: string) => {
}
export const addRoomMembership = async (url: string, room: string) => {
- const list = get(userMembership) || makeList({kind: ROOMS})
+ const list = get(userGroupSelections) || makeList({kind: ROOMS})
const newTags = [
["r", url],
["group", room, url],
@@ -204,7 +203,7 @@ export const addRoomMembership = async (url: string, room: string) => {
}
export const removeRoomMembership = async (url: string, room: string) => {
- const list = get(userMembership) || makeList({kind: ROOMS})
+ const list = get(userGroupSelections) || makeList({kind: ROOMS})
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
@@ -226,12 +225,7 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
return publishThunk({
event: makeEvent(list.kind, {tags}),
- relays: [
- url,
- ...INDEXER_RELAYS,
- ...Router.get().FromUser().getUrls(),
- ...userRoomsByUrl.get().keys(),
- ],
+ relays: [url, ...INDEXER_RELAYS, ...Router.get().FromUser().getUrls(), ...get(userSpaceUrls)],
})
}
@@ -248,11 +242,7 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
return publishThunk({
event: makeEvent(list.kind, {tags}),
- relays: [
- ...INDEXER_RELAYS,
- ...Router.get().FromUser().getUrls(),
- ...userRoomsByUrl.get().keys(),
- ],
+ relays: [...INDEXER_RELAYS, ...Router.get().FromUser().getUrls(), ...get(userSpaceUrls)],
})
}
}
@@ -632,7 +622,7 @@ export type JoinRequestParams = {
}
export const makeJoinRequest = (params: JoinRequestParams) =>
- makeEvent(AUTH_JOIN, {tags: [["claim", params.claim]]})
+ makeEvent(RELAY_JOIN, {tags: [["claim", params.claim]]})
export const publishJoinRequest = (params: JoinRequestParams) =>
publishThunk({event: makeJoinRequest(params), relays: [params.url]})
@@ -781,7 +771,7 @@ export const updateProfile = async ({
}) => {
const router = Router.get()
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
- const scenarios = [router.FromRelays(getMembershipUrls(userMembership.get()))]
+ const scenarios = [router.FromRelays(get(userSpaceUrls))]
if (shouldBroadcast) {
scenarios.push(router.FromUser(), router.Index())
diff --git a/src/app/core/requests.ts b/src/app/core/requests.ts
index 32c4859..2cef795 100644
--- a/src/app/core/requests.ts
+++ b/src/app/core/requests.ts
@@ -14,7 +14,7 @@ import {
} from "@welshman/lib"
import {
EVENT_TIME,
- AUTH_INVITE,
+ RELAY_INVITE,
ALERT_EMAIL,
ALERT_WEB,
ALERT_IOS,
@@ -29,7 +29,7 @@ import {
import type {TrustedEvent, Filter, List} from "@welshman/util"
import {feedFromFilters, makeRelayFeed, makeIntersectionFeed} from "@welshman/feeds"
import {load, request} from "@welshman/net"
-import {repository, makeFeedController, loadRelay} from "@welshman/app"
+import {repository, makeFeedController, loadRelay, tracker} from "@welshman/app"
import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util"
import {NOTIFIER_RELAY, getEventsForUrl} from "@app/core/state"
@@ -93,7 +93,7 @@ export const makeFeed = ({
}
for (const event of added) {
- if (matchFilters(filters, event)) {
+ if (matchFilters(filters, event) && tracker.getRelays(event.id).has(url)) {
insertEvent(event)
}
}
@@ -267,7 +267,7 @@ export const discoverRelays = (lists: List[]) =>
)
export const requestRelayClaim = async (url: string) => {
- const filters = [{kinds: [AUTH_INVITE], limit: 1}]
+ const filters = [{kinds: [RELAY_INVITE], limit: 1}]
const events = await load({filters, relays: [url]})
if (events.length > 0) {
diff --git a/src/app/core/state.ts b/src/app/core/state.ts
index cb490bc..7ccaf1a 100644
--- a/src/app/core/state.ts
+++ b/src/app/core/state.ts
@@ -4,7 +4,9 @@ import {get, derived, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {
on,
+ spec,
call,
+ first,
assoc,
remove,
uniqBy,
@@ -12,9 +14,7 @@ import {
sort,
prop,
uniq,
- nth,
pushToMapKey,
- nthEq,
shuffle,
parseJson,
fromPairs,
@@ -24,7 +24,6 @@ import {
groupBy,
always,
tryCatch,
- last,
} from "@welshman/lib"
import type {Socket} from "@welshman/net"
import {
@@ -46,49 +45,56 @@ import {
} from "@welshman/store"
import {isKindFeed, findFeed} from "@welshman/feeds"
import {
- getIdFilters,
- WRAP,
- DELETE,
+ ALERT_ANDROID,
+ ALERT_EMAIL,
+ ALERT_IOS,
+ ALERT_STATUS,
+ ALERT_WEB,
+ APP_DATA,
CLIENT_AUTH,
- AUTH_JOIN,
- REACTION,
- ZAP_REQUEST,
- ZAP_RESPONSE,
- DIRECT_MESSAGE,
+ COMMENT,
+ DELETE,
DIRECT_MESSAGE_FILE,
- ROOM_META,
+ DIRECT_MESSAGE,
+ EVENT_TIME,
MESSAGE,
+ REACTION,
+ RELAY_ADD_MEMBER,
+ RELAY_JOIN,
+ RELAY_LEAVE,
+ RELAY_MEMBERS,
+ RELAY_REMOVE_MEMBER,
+ REPORT,
+ ROOM_ADD_MEMBER,
+ ROOM_CREATE_PERMISSION,
+ ROOM_JOIN,
+ ROOM_LEAVE,
+ ROOM_MEMBERS,
+ ROOM_META,
+ ROOM_REMOVE_MEMBER,
ROOMS,
THREAD,
- COMMENT,
- REPORT,
- ROOM_JOIN,
- ROOM_ADD_USER,
- ROOM_REMOVE_USER,
- ROOM_CREATE_PERMISSION,
- ALERT_EMAIL,
- ALERT_WEB,
- ALERT_IOS,
- ALERT_ANDROID,
- ALERT_STATUS,
- APP_DATA,
+ WRAP,
ZAP_GOAL,
- EVENT_TIME,
- getGroupTags,
- getRelayTagValues,
- getPubkeyTagValues,
- displayProfile,
- readList,
- getListTags,
+ ZAP_REQUEST,
+ ZAP_RESPONSE,
asDecryptedEvent,
- normalizeRelayUrl,
+ displayProfile,
+ getGroupTags,
+ getIdFilters,
+ getListTags,
+ getPubkeyTagValues,
+ getRelaysFromList,
+ getRelayTagValues,
getTag,
getTagValue,
getTagValues,
- verifyEvent,
+ isRelayUrl,
makeEvent,
+ normalizeRelayUrl,
+ readList,
RelayMode,
- getRelaysFromList,
+ verifyEvent,
} from "@welshman/util"
import type {TrustedEvent, PublishedList, List, Filter} from "@welshman/util"
import {decrypt} from "@welshman/signer"
@@ -111,6 +117,9 @@ import {
publishThunk,
userRelaySelections,
userInboxRelaySelections,
+ deriveRelay,
+ makeUserData,
+ makeUserLoader,
} from "@welshman/app"
import type {Thunk, Relay} from "@welshman/app"
@@ -158,7 +167,7 @@ export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," +
- [CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, ROOMS, WRAP, REACTION, ZAP_REQUEST]
+ [CLIENT_AUTH, RELAY_JOIN, MESSAGE, THREAD, COMMENT, ROOMS, WRAP, REACTION, ZAP_REQUEST]
.map(k => `sign_event:${k}`)
.join(",")
@@ -191,7 +200,7 @@ export const entityLink = (entity: string) => `https://coracle.social/${entity}`
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
entityLink(nip19.nprofileEncode({pubkey, relays}))
-export const defaultPubkeys = derived(userFollows, $userFollows => {
+export const bootstrapPubkeys = derived(userFollows, $userFollows => {
const appPubkeys = DEFAULT_PUBKEYS.split(",")
const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollows)))
@@ -221,29 +230,31 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
)
}
-export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
- const getThunksByEventId = memoize(() => {
- const thunksByEventId = new Map()
+export const getUrlsForEvent = withGetter(
+ derived([trackerStore, thunks], ([$tracker, $thunks]) => {
+ const getThunksByEventId = memoize(() => {
+ const thunksByEventId = new Map()
- for (const thunk of $thunks) {
- pushToMapKey(thunksByEventId, thunk.event.id, thunk)
- }
-
- return thunksByEventId
- })
-
- return (id: string) => {
- const urls = Array.from($tracker.getRelays(id))
-
- for (const thunk of getThunksByEventId().get(id) || []) {
- for (const url of thunk.options.relays) {
- urls.push(url)
+ for (const thunk of $thunks) {
+ pushToMapKey(thunksByEventId, thunk.event.id, thunk)
}
- }
- return uniq(urls)
- }
-})
+ return thunksByEventId
+ })
+
+ return (id: string) => {
+ const urls = Array.from($tracker.getRelays(id))
+
+ for (const thunk of getThunksByEventId().get(id) || []) {
+ for (const url of thunk.options.relays) {
+ urls.push(url)
+ }
+ }
+
+ return uniq(urls)
+ }
+ }),
+)
export const getEventsForUrl = (url: string, filters: Filter[]) => {
const ids = uniq([
@@ -266,6 +277,11 @@ export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
return repository.query(filters.map(assoc("ids", ids)))
})
+export const deriveSignedEventsForUrl = (url: string, filters: Filter[]) =>
+ derived([deriveEventsForUrl(url, filters), deriveRelay(url)], ([$events, $relay]) =>
+ $relay?.profile ? $events.filter(spec({pubkey: $relay.profile.self})) : [],
+ )
+
// Context
appContext.dufflepudUrl = DUFFLEPUD_URL
@@ -295,6 +311,15 @@ export const MESSAGE_FILTER = {kinds: MESSAGE_KINDS}
export const COMMENT_FILTER = makeCommentFilter(MESSAGE_KINDS)
+export const MEMBERSHIP_KINDS = [
+ ROOM_ADD_MEMBER,
+ ROOM_REMOVE_MEMBER,
+ RELAY_ADD_MEMBER,
+ RELAY_REMOVE_MEMBER,
+]
+
+export const MEMBERSHIP_FILTER = {kinds: MEMBERSHIP_KINDS}
+
// Settings
export const SETTINGS = "flotilla/settings"
@@ -348,6 +373,19 @@ export const {
load: makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
})
+export const userSettings = makeUserData({
+ mapStore: settingsByPubkey,
+ loadItem: loadSettings,
+})
+
+export const loadUserSettings = makeUserLoader(loadSettings)
+
+export const userSettingsValues = withGetter(
+ derived(userSettings, $s => $s?.values || defaultSettings),
+)
+
+export const getSetting = (key: keyof Settings["values"]) => userSettingsValues.get()[key] as T
+
// Relays sending events with empty signatures that the user has to choose to trust
export const relaysPendingTrust = withGetter(writable([]))
@@ -428,64 +466,6 @@ export const alertStatuses = withGetter(
export const deriveAlertStatus = (address: string) =>
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))
-// Membership
-
-export const hasMembershipUrl = (list: List | undefined, url: string) =>
- getListTags(list).some(t => {
- if (t[0] === "r") return t[1] === url
- if (t[0] === "group") return t[2] === url
-
- return false
- })
-
-export const getMembershipUrls = (list?: List) => {
- const tags = getListTags(list)
-
- return sort(
- uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]).map(url =>
- normalizeRelayUrl(url),
- ),
- )
-}
-
-export const getMembershipRooms = (list?: List) =>
- getGroupTags(getListTags(list)).map(([_, room, url, name = ""]) => ({url, room, name}))
-
-export const getMembershipRoomsByUrl = (url: string, list?: List) =>
- sort(getGroupTags(getListTags(list)).filter(nthEq(2, url)).map(nth(1)))
-
-export const memberships = deriveEventsMapped(repository, {
- filters: [{kinds: [ROOMS]}],
- itemToEvent: item => item.event,
- eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
-})
-
-export const {
- indexStore: membershipsByPubkey,
- deriveItem: deriveMembership,
- loadItem: loadMembership,
-} = collection({
- name: "memberships",
- store: memberships,
- getKey: list => list.event.pubkey,
- load: makeOutboxLoader(ROOMS),
-})
-
-export const membersByUrl = derived(
- [defaultPubkeys, membershipsByPubkey],
- ([$defaultPubkeys, $membershipsByPubkey]) => {
- const $membersByUrl = new Map>()
-
- for (const pubkey of $defaultPubkeys) {
- for (const url of getMembershipUrls($membershipsByPubkey.get(pubkey))) {
- addToMapKey($membersByUrl, url, pubkey)
- }
- }
-
- return $membersByUrl
- },
-)
-
// Chats
export const chatMessages = deriveEvents(repository, {
@@ -556,12 +536,10 @@ export const chatSearch = derived(chats, $chats =>
}),
)
-// Messages
+// Channels
export const messages = deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]})
-// Channels
-
export type Channel = {
id: string
url: string
@@ -645,98 +623,243 @@ export const displayChannel = (url: string, room: string) =>
export const roomComparator = (url: string) => (room: string) =>
displayChannel(url, room).toLowerCase()
-// User stuff
+// User space/room selections
-export const userSettings = withGetter(
- derived([pubkey, settingsByPubkey], ([$pubkey, $settingsByPubkey]) => {
- if (!$pubkey) return undefined
+export const groupSelections = deriveEventsMapped(repository, {
+ filters: [{kinds: [ROOMS]}],
+ itemToEvent: item => item.event,
+ eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
+})
- loadSettings($pubkey)
+export const {
+ indexStore: groupSelectionsByPubkey,
+ deriveItem: deriveGroupSelections,
+ loadItem: loadGroupSelections,
+} = collection({
+ name: "groupSelections",
+ store: groupSelections,
+ getKey: list => list.event.pubkey,
+ load: makeOutboxLoader(ROOMS),
+})
- return $settingsByPubkey.get($pubkey)
- }),
-)
+export const groupSelectionsPubkeysByUrl = derived(groupSelections, $groupSelections => {
+ const result = new Map>()
-export const userSettingsValues = withGetter(
- derived(userSettings, $s => $s?.values || defaultSettings),
-)
-
-export const getSetting = (key: keyof Settings["values"]) => userSettingsValues.get()[key] as T
-
-export const userMembership = withGetter(
- derived([pubkey, membershipsByPubkey], ([$pubkey, $membershipsByPubkey]) => {
- if (!$pubkey) return undefined
-
- loadMembership($pubkey)
-
- return $membershipsByPubkey.get($pubkey)
- }),
-)
-
-export const userRoomsByUrl = withGetter(
- derived([userMembership, channelsById], ([$userMembership, $channelsById]) => {
- const tags = getListTags($userMembership)
- const $userRoomsByUrl = new Map>()
+ for (const list of $groupSelections) {
+ const tags = getListTags(list)
for (const url of getRelayTagValues(tags)) {
- $userRoomsByUrl.set(normalizeRelayUrl(url), new Set())
+ addToMapKey(result, url, list.event.pubkey)
}
- for (const [_, room, url] of getGroupTags(tags)) {
+ for (const tag of getGroupTags(tags)) {
+ const url = tag[2] || ""
+
+ if (isRelayUrl(url)) {
+ addToMapKey(result, url, list.event.pubkey)
+ }
+ }
+ }
+
+ return result
+})
+
+export const getSpaceUrlsFromGroupSelections = ($groupSelections: List | undefined) => {
+ const tags = getListTags($groupSelections)
+ const urls = getRelayTagValues(tags)
+
+ for (const tag of getGroupTags(tags)) {
+ const url = tag[2] || ""
+
+ if (isRelayUrl(url)) {
+ urls.push(url)
+ }
+ }
+
+ return uniq(urls.map(normalizeRelayUrl))
+}
+
+export const getSpaceRoomsFromGroupSelections = (
+ url: string,
+ $groupSelections: List | undefined,
+) => {
+ const rooms: string[] = []
+
+ for (const [_, room, relay] of getGroupTags(getListTags($groupSelections))) {
+ if (url === relay) {
+ rooms.push(room)
+ }
+ }
+
+ return sortBy(roomComparator(url), rooms)
+}
+
+export const userGroupSelections = makeUserData({
+ mapStore: groupSelectionsByPubkey,
+ loadItem: loadGroupSelections,
+})
+
+export const loadUserGroupSelections = makeUserLoader(loadGroupSelections)
+
+export const userSpaceUrls = derived(userGroupSelections, getSpaceUrlsFromGroupSelections)
+
+export const deriveUserRooms = (url: string) =>
+ derived([userGroupSelections, channelsById], ([$userGroupSelections, $channelsById]) => {
+ const rooms: string[] = []
+
+ for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
if ($channelsById.has(makeChannelId(url, room))) {
- addToMapKey($userRoomsByUrl, normalizeRelayUrl(url), room)
+ rooms.push(room)
}
}
- return $userRoomsByUrl
- }),
-)
-
-export const deriveUserRooms = (url: string) =>
- derived(userRoomsByUrl, $userRoomsByUrl =>
- sortBy(roomComparator(url), uniq(Array.from($userRoomsByUrl.get(url) || []))),
- )
+ return sortBy(roomComparator(url), rooms)
+ })
export const deriveOtherRooms = (url: string) =>
- derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) =>
- sortBy(
- roomComparator(url),
- ($channelsByUrl.get(url) || []).filter(c => !$userRooms.includes(c.room)).map(c => c.room),
- ),
+ derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) => {
+ const rooms: string[] = []
+
+ for (const {room} of $channelsByUrl.get(url) || []) {
+ if (!$userRooms.includes(room)) {
+ rooms.push(room)
+ }
+ }
+
+ return sortBy(roomComparator(url), rooms)
+ })
+
+// Space/room memberships
+
+export const deriveSpaceMembers = (url: string) =>
+ derived(
+ deriveSignedEventsForUrl(url, [
+ {kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]},
+ ]),
+ $events => {
+ const membersEvent = $events.find(spec({kind: RELAY_MEMBERS}))
+
+ if (membersEvent) {
+ return getTagValues("member", membersEvent.tags)
+ }
+
+ const members = new Set()
+
+ for (const event of $events) {
+ const pubkeys = getPubkeyTagValues(event.tags)
+
+ if (event.kind === RELAY_ADD_MEMBER) {
+ for (const pubkey of pubkeys) {
+ members.add(pubkey)
+ }
+ }
+
+ if (event.kind === RELAY_REMOVE_MEMBER) {
+ for (const pubkey of pubkeys) {
+ members.delete(pubkey)
+ }
+ }
+ }
+
+ return Array.from(members)
+ },
)
+export const deriveRoomMembers = (url: string, room: string) =>
+ derived(
+ deriveEventsForUrl(url, [
+ {kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER, ROOM_MEMBERS], "#h": [room]},
+ ]),
+ $events => {
+ const membersEvent = $events.find(spec({kind: ROOM_MEMBERS}))
+
+ if (membersEvent) {
+ return getPubkeyTagValues(membersEvent.tags)
+ }
+
+ const members = new Set()
+
+ for (const event of $events) {
+ const pubkeys = getPubkeyTagValues(event.tags)
+
+ if (event.kind === ROOM_ADD_MEMBER) {
+ for (const pubkey of pubkeys) {
+ members.add(pubkey)
+ }
+ }
+
+ if (event.kind === ROOM_REMOVE_MEMBER) {
+ for (const pubkey of pubkeys) {
+ members.delete(pubkey)
+ }
+ }
+ }
+
+ return Array.from(members)
+ },
+ )
+
+// User membership status
+
export enum MembershipStatus {
Initial,
Pending,
Granted,
}
-export const deriveUserMembershipStatus = (url: string, room: string) =>
+export const deriveUserSpaceMembershipStatus = (url: string) =>
derived(
[
pubkey,
- deriveEventsForUrl(url, [
- {kinds: [ROOM_JOIN, ROOM_ADD_USER, ROOM_REMOVE_USER], "#h": [room]},
- ]),
+ deriveSpaceMembers(url),
+ deriveEventsForUrl(url, [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]),
],
- ([$pubkey, $events]) => {
- let status = MembershipStatus.Initial
+ ([$pubkey, $members, $events]) => {
+ const isMember = $members.includes($pubkey)
for (const event of $events) {
- if (event.kind === ROOM_JOIN && event.pubkey === $pubkey) {
- status = MembershipStatus.Pending
+ if (event.pubkey !== $pubkey) {
+ continue
}
- if (event.kind === ROOM_REMOVE_USER && getTagValues("p", event.tags).includes($pubkey!)) {
- break
+ if (event.kind === RELAY_JOIN) {
+ return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
}
- if (event.kind === ROOM_ADD_USER && getTagValues("p", event.tags).includes($pubkey!)) {
- return MembershipStatus.Granted
+ if (event.kind === RELAY_LEAVE) {
+ return MembershipStatus.Initial
}
}
- return status
+ return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
+ },
+ )
+
+export const deriveUserRoomMembershipStatus = (url: string, room: string) =>
+ derived(
+ [
+ pubkey,
+ deriveRoomMembers(url, room),
+ deriveEventsForUrl(url, [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [room]}]),
+ ],
+ ([$pubkey, $members, $events]) => {
+ const isMember = $members.includes($pubkey)
+
+ for (const event of $events) {
+ if (event.pubkey !== $pubkey) {
+ continue
+ }
+
+ if (event.kind === ROOM_JOIN) {
+ return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
+ }
+
+ if (event.kind === ROOM_LEAVE) {
+ return MembershipStatus.Initial
+ }
+ }
+
+ return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
},
)
@@ -744,13 +867,9 @@ export const deriveUserCanCreateRoom = (url: string) =>
derived(
[pubkey, deriveEventsForUrl(url, [{kinds: [ROOM_CREATE_PERMISSION]}])],
([$pubkey, $events]) => {
- const latest = last($events)
+ const event = first($events)
- if (!latest) {
- return true
- }
-
- return getTagValues("p", latest.tags).includes($pubkey!)
+ return event ? getPubkeyTagValues(event.tags).includes($pubkey!) : true
},
)
@@ -853,7 +972,7 @@ export const deriveRelayAuthError = (url: string, claim = "") => {
// Attempt to join the relay
const thunk = publishThunk({
- event: makeEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
+ event: makeEvent(RELAY_JOIN, {tags: [["claim", claim]]}),
relays: [url],
})
diff --git a/src/app/core/sync.ts b/src/app/core/sync.ts
index a6f71d5..e5a1fda 100644
--- a/src/app/core/sync.ts
+++ b/src/app/core/sync.ts
@@ -19,10 +19,9 @@ import {
getRelayTagValues,
WRAP,
ROOM_META,
- ROOM_ADD_USER,
- ROOM_REMOVE_USER,
+ ROOM_ADD_MEMBER,
+ ROOM_REMOVE_MEMBER,
isSignedEvent,
- normalizeRelayUrl,
} from "@welshman/util"
import type {Filter, TrustedEvent} from "@welshman/util"
import {request, load, pull} from "@welshman/net"
@@ -45,13 +44,14 @@ import {
import {
MESSAGE_FILTER,
COMMENT_FILTER,
+ MEMBERSHIP_FILTER,
INDEXER_RELAYS,
REACTION_KINDS,
loadSettings,
- userMembership,
- defaultPubkeys,
+ loadGroupSelections,
+ userSpaceUrls,
+ bootstrapPubkeys,
decodeRelay,
- loadMembership,
getUrlsForEvent,
} from "@app/core/state"
import {loadAlerts, loadAlertStatuses} from "@app/core/requests"
@@ -102,15 +102,15 @@ const syncRelays = () => {
}
})
- const unsubscribeMembership = userMembership.subscribe($l => {
- for (const url of getRelayTagValues(getListTags($l))) {
+ const unsubscribeSpaceUrls = userSpaceUrls.subscribe(urls => {
+ for (const url of urls) {
loadRelay(url)
}
})
return () => {
unsubscribePage()
- unsubscribeMembership()
+ unsubscribeSpaceUrls()
}
}
@@ -131,7 +131,7 @@ const syncUserData = () => {
loadAlertStatuses($pubkey)
loadBlossomServers($pubkey)
loadFollows($pubkey)
- loadMembership($pubkey)
+ loadGroupSelections($pubkey)
loadMutes($pubkey)
loadProfile($pubkey)
loadSettings($pubkey)
@@ -139,13 +139,13 @@ const syncUserData = () => {
})
const unsubscribeFollows = userFollows.subscribe(async $l => {
- for (const pubkeys of chunk(10, get(defaultPubkeys))) {
+ for (const pubkeys of chunk(10, get(bootstrapPubkeys))) {
// This isn't urgent, avoid clogging other stuff up
await sleep(1000)
for (const pk of pubkeys) {
loadRelaySelections(pk).then(() => {
- loadMembership(pk)
+ loadGroupSelections(pk)
loadProfile(pk)
loadFollows(pk)
loadMutes(pk)
@@ -177,14 +177,14 @@ const syncMembership = (url: string) => {
pullConservatively({
relays: [url],
signal: controller.signal,
- filters: [MESSAGE_FILTER, COMMENT_FILTER].map(assoc("since", ago(MONTH))),
+ filters: [MESSAGE_FILTER, COMMENT_FILTER, MEMBERSHIP_FILTER].map(assoc("since", ago(MONTH))),
})
// Listen for new events
request({
relays: [url],
signal: controller.signal,
- filters: [MESSAGE_FILTER, COMMENT_FILTER].map(assoc("since", now())),
+ filters: [MESSAGE_FILTER, COMMENT_FILTER, MEMBERSHIP_FILTER].map(assoc("since", now())),
})
return () => controller.abort()
@@ -193,9 +193,7 @@ const syncMembership = (url: string) => {
const syncMemberships = () => {
const unsubscribersByUrl = new Map()
- const unsubscribeMembership = userMembership.subscribe($l => {
- const urls = getRelayTagValues(getListTags($l)).map(normalizeRelayUrl)
-
+ const unsubscribeSpaceUrls = userSpaceUrls.subscribe(urls => {
// stop syncing removed spaces
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
if (!urls.includes(url)) {
@@ -214,7 +212,7 @@ const syncMemberships = () => {
return () => {
Array.from(unsubscribersByUrl.values()).forEach(call)
- unsubscribeMembership()
+ unsubscribeSpaceUrls()
}
}
@@ -231,7 +229,7 @@ const syncSpace = (url: string) => {
signal: controller.signal,
filters: [
{
- kinds: [ROOM_ADD_USER, ROOM_REMOVE_USER],
+ kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER],
"#p": [$pubkey],
},
],
@@ -244,7 +242,7 @@ const syncSpace = (url: string) => {
signal: controller.signal,
filters: [
{
- kinds: [ROOM_ADD_USER, ROOM_REMOVE_USER, ...REACTION_KINDS],
+ kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER, ...REACTION_KINDS],
since: now(),
},
],
diff --git a/src/app/util/notifications.ts b/src/app/util/notifications.ts
index c104f3d..bdc4bd3 100644
--- a/src/app/util/notifications.ts
+++ b/src/app/util/notifications.ts
@@ -17,9 +17,11 @@ import {
chats,
hasNip29,
getUrlsForEvent,
- userRoomsByUrl,
repositoryStore,
userSettingsValues,
+ userGroupSelections,
+ getSpaceUrlsFromGroupSelections,
+ getSpaceRoomsFromGroupSelections,
} from "@app/core/state"
import {preferencesStorageProvider} from "@src/lib/storage"
import {Badge} from "@capawesome/capacitor-badge"
@@ -42,11 +44,19 @@ export const notifications = derived(
throttled(
1000,
derived(
- [pubkey, checked, chats, userRoomsByUrl, repositoryStore, getUrlsForEvent, relaysByUrl],
+ [pubkey, checked, chats, userGroupSelections, repositoryStore, getUrlsForEvent, relaysByUrl],
identity,
),
),
- ([$pubkey, $checked, $chats, $userRoomsByUrl, $repository, $getUrlsForEvent, $relaysByUrl]) => {
+ ([
+ $pubkey,
+ $checked,
+ $chats,
+ $userGroupSelections,
+ $repository,
+ $getUrlsForEvent,
+ $relaysByUrl,
+ ]) => {
const hasNotification = (path: string, latestEvent: TrustedEvent | undefined) => {
if (!latestEvent || latestEvent.pubkey === $pubkey) {
return false
@@ -85,7 +95,7 @@ export const notifications = derived(
const allMessages = $repository.query([{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}])
- for (const [url, rooms] of $userRoomsByUrl.entries()) {
+ for (const url of getSpaceUrlsFromGroupSelections($userGroupSelections)) {
const spacePath = makeSpacePath(url)
const spacePathMobile = spacePath + ":mobile"
const goalPath = makeGoalPath(url)
@@ -161,7 +171,7 @@ export const notifications = derived(
}
if (hasNip29($relaysByUrl.get(url))) {
- for (const room of rooms) {
+ for (const room of getSpaceRoomsFromGroupSelections(url, $userGroupSelections)) {
const roomPath = makeRoomPath(url, room)
const latestEvent = allMessages.find(
e => $getUrlsForEvent(e.id).includes(url) && e.tags.find(spec(["h", room])),
diff --git a/src/app/util/routes.ts b/src/app/util/routes.ts
index 18a201d..1a14cae 100644
--- a/src/app/util/routes.ts
+++ b/src/app/util/routes.ts
@@ -1,4 +1,5 @@
import type {Page} from "@sveltejs/kit"
+import {get} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {goto} from "$app/navigation"
import {nthEq, sleep} from "@welshman/lib"
@@ -21,7 +22,7 @@ import {
entityLink,
decodeRelay,
encodeRelay,
- userRoomsByUrl,
+ userSpaceUrls,
hasNip29,
ROOM,
} from "@app/core/state"
@@ -66,7 +67,7 @@ export const makeCalendarPath = (url: string, eventId?: string) =>
export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1]
export const getPrimaryNavItemIndex = ($page: Page) => {
- const urls = Array.from(userRoomsByUrl.get().keys())
+ const urls = get(userSpaceUrls)
switch (getPrimaryNavItem($page)) {
case "discover":
diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte
index 21af285..34040d2 100644
--- a/src/routes/discover/+page.svelte
+++ b/src/routes/discover/+page.svelte
@@ -6,7 +6,7 @@
import {Router} from "@welshman/router"
import {load} from "@welshman/net"
import type {Relay} from "@welshman/app"
- import {relays, createSearch, loadRelay, loadRelaySelections} from "@welshman/app"
+ import {relays, createSearch, loadRelay} from "@welshman/app"
import {createScroller} from "@lib/html"
import {fly} from "@lib/transition"
import QrCode from "@assets/icons/qr-code.svg?dataurl"
@@ -23,7 +23,12 @@
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
import RelaySummary from "@app/components/RelaySummary.svelte"
import SpaceCheck from "@app/components/SpaceCheck.svelte"
- import {getMembershipUrls, loadMembership, defaultPubkeys, membersByUrl} from "@app/core/state"
+ import {
+ bootstrapPubkeys,
+ loadGroupSelections,
+ getSpaceUrlsFromGroupSelections,
+ groupSelectionsPubkeysByUrl,
+ } from "@app/core/state"
import {pushModal} from "@app/util/modal"
const openMenu = () => pushModal(SpaceAdd)
@@ -45,11 +50,9 @@
filters: [{kinds: [ROOMS]}],
relays: Router.get().Index().getUrls(),
}),
- ...$defaultPubkeys.map(async pubkey => {
- await loadRelaySelections(pubkey)
-
- const membership = await loadMembership(pubkey)
- const urls = getMembershipUrls(membership)
+ ...$bootstrapPubkeys.map(async pubkey => {
+ const list = await loadGroupSelections(pubkey)
+ const urls = getSpaceUrlsFromGroupSelections(list)
await Promise.all(urls.map(url => loadRelay(url)))
}),
@@ -57,13 +60,13 @@
const relaySearch = $derived(
createSearch(
- $relays.filter(r => $membersByUrl.has(r.url) && r.url !== termUrl),
+ $relays.filter(r => $groupSelectionsPubkeysByUrl.has(r.url) && r.url !== termUrl),
{
getValue: (relay: Relay) => relay.url,
sortFn: ({score, item}) => {
if (score && score > 0.1) return -score!
- const wotScore = $membersByUrl.get(item.url)?.size || 0
+ const wotScore = $groupSelectionsPubkeysByUrl.get(item.url)!.size
return score ? dec(score) * wotScore : -wotScore
},
diff --git a/src/routes/people/+page.svelte b/src/routes/people/+page.svelte
index 3effc34..694a7d9 100644
--- a/src/routes/people/+page.svelte
+++ b/src/routes/people/+page.svelte
@@ -7,13 +7,13 @@
import Page from "@lib/components/Page.svelte"
import ContentSearch from "@lib/components/ContentSearch.svelte"
import PeopleItem from "@app/components/PeopleItem.svelte"
- import {defaultPubkeys} from "@app/core/state"
+ import {bootstrapPubkeys} from "@app/core/state"
let term = $state("")
let limit = $state(10)
let element: Element | undefined = $state()
- const pubkeys = $derived(term ? $profileSearch.searchValues(term) : $defaultPubkeys)
+ const pubkeys = $derived(term ? $profileSearch.searchValues(term) : $bootstrapPubkeys)
onMount(() => {
const scroller = createScroller({
diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte
index 16094fb..565ad14 100644
--- a/src/routes/spaces/[relay]/[room]/+page.svelte
+++ b/src/routes/spaces/[relay]/[room]/+page.svelte
@@ -7,7 +7,13 @@
import type {MakeNonOptional} from "@welshman/lib"
import {now, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
- import {makeEvent, makeRoomMeta, MESSAGE} from "@welshman/util"
+ import {
+ makeEvent,
+ makeRoomMeta,
+ MESSAGE,
+ ROOM_ADD_MEMBER,
+ ROOM_REMOVE_MEMBER,
+ } from "@welshman/util"
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
import {slide, fade, fly} from "@lib/transition"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
@@ -26,13 +32,16 @@
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ChannelName from "@app/components/ChannelName.svelte"
import ChannelItem from "@app/components/ChannelItem.svelte"
+ import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte"
+ import ChannelItemRemoveMember from "@src/app/components/ChannelItemRemoveMember.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte"
+ import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {
- userRoomsByUrl,
+ deriveUserRooms,
userSettingsValues,
decodeRelay,
- deriveUserMembershipStatus,
+ deriveUserRoomMembershipStatus,
deriveChannel,
MembershipStatus,
PROTECTED,
@@ -49,16 +58,16 @@
import {makeFeed} from "@app/core/requests"
import {popKey} from "@lib/implicit"
import {pushToast} from "@app/util/toast"
- import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte"
const {room, relay} = $page.params as MakeNonOptional
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay(relay)
const channel = deriveChannel(url, room)
- const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
const shouldProtect = canEnforceNip70(url)
- const membershipStatus = deriveUserMembershipStatus(url, room)
+ const userRooms = deriveUserRooms(url)
+ const isFavorite = $derived($userRooms.includes(room))
+ const membershipStatus = deriveUserRoomMembershipStatus(url, room)
const addFavorite = () => addRoomMembership(url, room)
@@ -256,7 +265,7 @@
const feed = makeFeed({
url,
element: element!,
- filters: [{kinds: MESSAGE_KINDS, "#h": [room]}],
+ filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [room]}],
onExhausted: () => {
loadingEvents = false
},
@@ -398,15 +407,22 @@
{:else if type === "date"}
{value}
{:else}
-
-
-
+ {@const event = $state.snapshot(value as TrustedEvent)}
+ {#if event.kind === ROOM_ADD_MEMBER}
+
+ {:else if event.kind === ROOM_REMOVE_MEMBER}
+
+ {:else}
+
+
+
+ {/if}
{/if}
{/each}
diff --git a/src/routes/spaces/[relay]/chat/+page.svelte b/src/routes/spaces/[relay]/chat/+page.svelte
index 19a0d10..af41400 100644
--- a/src/routes/spaces/[relay]/chat/+page.svelte
+++ b/src/routes/spaces/[relay]/chat/+page.svelte
@@ -5,9 +5,9 @@
import {readable} from "svelte/store"
import {now, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
- import {makeEvent, MESSAGE} from "@welshman/util"
+ import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER} from "@welshman/util"
import {pubkey, publishThunk} from "@welshman/app"
- import {slide, fade, fly} from "@lib/transition"
+ import {fade, fly} from "@lib/transition"
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -19,15 +19,17 @@
import ThunkToast from "@app/components/ThunkToast.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ChannelItem from "@app/components/ChannelItem.svelte"
+ import ChannelItemAddMember from "@src/app/components/ChannelItemAddMember.svelte"
+ import ChannelItemRemoveMember from "@src/app/components/ChannelItemRemoveMember.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte"
+ import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
- import {userSettingsValues, decodeRelay, MESSAGE_FILTER, PROTECTED} from "@app/core/state"
+ import {userSettingsValues, decodeRelay, PROTECTED, MESSAGE_KINDS} from "@app/core/state"
import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands"
import {setChecked, checked} from "@app/util/notifications"
import {pushToast} from "@app/util/toast"
import {makeFeed} from "@app/core/requests"
import {popKey} from "@lib/implicit"
- import ChannelComposeEdit from "@src/app/components/ChannelComposeEdit.svelte"
const mounted = now()
const lastChecked = $checked[$page.url.pathname]
@@ -218,7 +220,7 @@
const feed = makeFeed({
url,
element: element!,
- filters: [MESSAGE_FILTER],
+ filters: [{kinds: [...MESSAGE_KINDS, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER]}],
onExhausted: () => {
loadingEvents = false
},
@@ -271,15 +273,21 @@
{value}
{:else}
{@const event = $state.snapshot(value as TrustedEvent)}
-
-
-
+ {#if event.kind === RELAY_ADD_MEMBER}
+
+ {:else if event.kind === RELAY_REMOVE_MEMBER}
+
+ {:else}
+
+
+
+ {/if}
{/if}
{/each}
diff --git a/tailwind.config.js b/tailwind.config.js
index 77a9a61..ea7fe03 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -9,6 +9,10 @@ config({path: ".env.template"})
export default {
content: ["./src/**/*.{html,js,svelte,ts}"],
darkMode: ['selector', '[data-theme="dark"]'],
+ safelist: [
+ 'bg-success',
+ 'bg-warning',
+ ],
theme: {
extend: {},
zIndex: {