forked from coracle/flotilla
Add member lists, use member lists to build room lists
This commit is contained in:
+9
-4
@@ -18,6 +18,7 @@ import {
|
|||||||
getListTags,
|
getListTags,
|
||||||
getRelayTags,
|
getRelayTags,
|
||||||
isShareableRelayUrl,
|
isShareableRelayUrl,
|
||||||
|
getRelayTagValues,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
|
import type {TrustedEvent, EventTemplate, List} from "@welshman/util"
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
@@ -145,31 +146,35 @@ export const broadcastUserData = async (relays: string[]) => {
|
|||||||
export const addSpaceMembership = async (url: string) => {
|
export const addSpaceMembership = async (url: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
||||||
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
||||||
|
const relays = [...ctx.app.router.WriteRelays().getUrls(), ...getRelayTagValues(event.tags)]
|
||||||
|
|
||||||
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
|
return publishThunk({event, relays}).result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeSpaceMembership = async (url: string) => {
|
export const removeSpaceMembership = async (url: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
||||||
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||||
|
const relays = [...ctx.app.router.WriteRelays().getUrls(), ...getRelayTagValues(event.tags)]
|
||||||
|
|
||||||
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
|
return publishThunk({event, relays}).result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addRoomMembership = async (url: string, room: string) => {
|
export const addRoomMembership = async (url: string, room: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
||||||
const event = await addToListPublicly(list, tagRoom(room, url)).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, tagRoom(room, url)).reconcile(nip44EncryptToSelf)
|
||||||
|
const relays = [...ctx.app.router.WriteRelays().getUrls(), ...getRelayTagValues(event.tags)]
|
||||||
|
|
||||||
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
|
return publishThunk({event, relays}).result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeRoomMembership = async (url: string, room: string) => {
|
export const removeRoomMembership = async (url: string, room: string) => {
|
||||||
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
|
||||||
const pred = (t: string[]) => equals(tagRoom(room, url), t)
|
const pred = (t: string[]) => equals(tagRoom(room, url), t)
|
||||||
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
||||||
|
const relays = [...ctx.app.router.WriteRelays().getUrls(), ...getRelayTagValues(event.tags)]
|
||||||
|
|
||||||
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
|
return publishThunk({event, relays}).result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
|
||||||
|
|||||||
@@ -11,11 +11,14 @@
|
|||||||
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
import SpaceInvite from "@app/components/SpaceInvite.svelte"
|
||||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
|
import ProfileList from "@app/components/ProfileList.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import {
|
import {
|
||||||
getMembershipRoomsByUrl,
|
getMembershipRoomsByUrl,
|
||||||
getMembershipUrls,
|
getMembershipUrls,
|
||||||
|
hasMembershipUrl,
|
||||||
userMembership,
|
userMembership,
|
||||||
|
memberships,
|
||||||
roomsByUrl,
|
roomsByUrl,
|
||||||
GENERAL,
|
GENERAL,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
@@ -34,6 +37,9 @@
|
|||||||
showMenu = !showMenu
|
showMenu = !showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showMembers = () =>
|
||||||
|
pushModal(ProfileList, {pubkeys: members, title: `Members of`, subtitle: displayRelayUrl(url)})
|
||||||
|
|
||||||
const createInvite = () => pushModal(SpaceInvite, {url})
|
const createInvite = () => pushModal(SpaceInvite, {url})
|
||||||
|
|
||||||
const leaveSpace = () => pushModal(SpaceExit, {url})
|
const leaveSpace = () => pushModal(SpaceExit, {url})
|
||||||
@@ -57,6 +63,7 @@
|
|||||||
|
|
||||||
$: rooms = getMembershipRoomsByUrl(url, $userMembership)
|
$: rooms = getMembershipRoomsByUrl(url, $userMembership)
|
||||||
$: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room))
|
$: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room))
|
||||||
|
$: members = $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey)
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const error = (await checkRelayConnection(url)) || (await checkRelayAuth(url))
|
const error = (await checkRelayConnection(url)) || (await checkRelayAuth(url))
|
||||||
@@ -78,6 +85,12 @@
|
|||||||
<ul
|
<ul
|
||||||
transition:fly
|
transition:fly
|
||||||
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
|
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
|
||||||
|
<li>
|
||||||
|
<Button on:click={showMembers}>
|
||||||
|
<Icon icon="user-rounded" />
|
||||||
|
View Members ({members.length})
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Button on:click={createInvite}>
|
<Button on:click={createInvite}>
|
||||||
<Icon icon="link-round" />
|
<Icon icon="link-round" />
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="border-top fixed bottom-0 left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
class="border-top fixed bottom-0 left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
|
||||||
<div class="flex content-padding-x content-sizing justify-between px-2">
|
<div class="content-padding-x content-sizing flex justify-between px-2">
|
||||||
<div class="flex gap-4 sm:gap-8">
|
<div class="flex gap-4 sm:gap-8">
|
||||||
<PrimaryNavItem title="Search" href="/people">
|
<PrimaryNavItem title="Search" href="/people">
|
||||||
<Avatar icon="magnifer" class="!h-10 !w-10" />
|
<Avatar icon="magnifer" class="!h-10 !w-10" />
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
|
||||||
|
export let title
|
||||||
|
export let subtitle = ""
|
||||||
export let pubkeys
|
export let pubkeys
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="row-2">
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">{title}</div>
|
||||||
|
<div slot="info">{subtitle}</div>
|
||||||
|
</ModalHeader>
|
||||||
{#each pubkeys as pubkey (pubkey)}
|
{#each pubkeys as pubkey (pubkey)}
|
||||||
<Profile {pubkey} />
|
<div class="card2 bg-alt">
|
||||||
|
<Profile {pubkey} />
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+37
-11
@@ -275,8 +275,21 @@ export const {
|
|||||||
|
|
||||||
// Membership
|
// Membership
|
||||||
|
|
||||||
|
export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
||||||
|
getListTags(list).some(t => {
|
||||||
|
if (t[0] === "r") return t[1] === url
|
||||||
|
if (t[0] === "~") return t[2] === url
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getListTags(list)))
|
export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getListTags(list)))
|
||||||
|
|
||||||
|
export const getMembershipRooms = (list?: List) =>
|
||||||
|
getListTags(list)
|
||||||
|
.filter(t => t[0] === "~")
|
||||||
|
.map(t => ({url: t[2], room: t[1]}))
|
||||||
|
|
||||||
export const getMembershipRoomsByUrl = (url: string, list?: List) =>
|
export const getMembershipRoomsByUrl = (url: string, list?: List) =>
|
||||||
sort(
|
sort(
|
||||||
getListTags(list)
|
getListTags(list)
|
||||||
@@ -320,7 +333,9 @@ export const readMessage = (event: TrustedEvent): Maybe<ChannelMessage> => {
|
|||||||
|
|
||||||
const [_, room, url] = roomTags[0]
|
const [_, room, url] = roomTags[0]
|
||||||
|
|
||||||
return {url, room, event}
|
if (!url || !room) return undefined
|
||||||
|
|
||||||
|
return {url: normalizeRelayUrl(url), room, event}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const channelMessages = deriveEventsMapped<ChannelMessage>(repository, {
|
export const channelMessages = deriveEventsMapped<ChannelMessage>(repository, {
|
||||||
@@ -342,19 +357,30 @@ export const makeChannelId = (url: string, room: string) => `${url}|${room}`
|
|||||||
|
|
||||||
export const splitChannelId = (id: string) => id.split("|")
|
export const splitChannelId = (id: string) => id.split("|")
|
||||||
|
|
||||||
export const channels = derived(channelMessages, $channelMessages => {
|
export const channels = derived(
|
||||||
const messagesByChannelId = new Map<string, ChannelMessage[]>()
|
[memberships, channelMessages],
|
||||||
|
([$memberships, $channelMessages]) => {
|
||||||
|
const messagesByChannelId = new Map<string, ChannelMessage[]>()
|
||||||
|
|
||||||
for (const message of $channelMessages) {
|
// Add known rooms by membership so we don't have to scan messages to load all rooms
|
||||||
pushToMapKey(messagesByChannelId, makeChannelId(message.url, message.room), message)
|
for (const membership of $memberships) {
|
||||||
}
|
for (const {url, room} of getMembershipRooms(membership)) {
|
||||||
|
messagesByChannelId.set(makeChannelId(url, room), [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Array.from(messagesByChannelId.entries()).map(([id, messages]) => {
|
// Add messages/rooms without memberships
|
||||||
const [url, room] = splitChannelId(id)
|
for (const message of $channelMessages) {
|
||||||
|
pushToMapKey(messagesByChannelId, makeChannelId(message.url, message.room), message)
|
||||||
|
}
|
||||||
|
|
||||||
return {id, url, room, messages}
|
return Array.from(messagesByChannelId.entries()).map(([id, messages]) => {
|
||||||
})
|
const [url, room] = splitChannelId(id)
|
||||||
})
|
|
||||||
|
return {id, url, room, messages}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
indexStore: channelsById,
|
indexStore: channelsById,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<div class="col-2 h-full content-padding-t content-padding-x {$$props.class}">
|
<div class="col-2 content-padding-t content-padding-x h-full {$$props.class}">
|
||||||
<div class="z-feature">
|
<div class="z-feature">
|
||||||
<div class="content-sizing">
|
<div class="content-sizing">
|
||||||
<slot name="input" />
|
<slot name="input" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-auto pt-2 scroll-container">
|
<div class="scroll-container overflow-auto pt-2">
|
||||||
<div class="content-sizing">
|
<div class="content-sizing">
|
||||||
<slot name="content" />
|
<slot name="content" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,23 +49,28 @@
|
|||||||
<label class="input input-bordered flex flex-grow items-center gap-2">
|
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||||
<Icon icon="magnifer" />
|
<Icon icon="magnifer" />
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<input autofocus bind:value={term} class="grow" type="text" placeholder="Search for conversations..." />
|
<input
|
||||||
|
autofocus
|
||||||
|
bind:value={term}
|
||||||
|
class="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search for conversations..." />
|
||||||
</label>
|
</label>
|
||||||
<Button class="btn btn-primary" on:click={startChat}>
|
<Button class="btn btn-primary" on:click={startChat}>
|
||||||
<Icon icon="add-circle" />
|
<Icon icon="add-circle" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div slot="content" class="col-2">
|
<div slot="content" class="col-2">
|
||||||
{#each chats as { id, pubkeys, messages } (id)}
|
{#each chats as { id, pubkeys, messages } (id)}
|
||||||
<ChatItem {id} {pubkeys} {messages} class="bg-alt card2" />
|
<ChatItem {id} {pubkeys} {messages} class="bg-alt card2" />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="py-20 max-w-sm col-4 items-center m-auto text-center">
|
<div class="py-20 max-w-sm col-4 items-center m-auto text-center">
|
||||||
<p>No chats found! Try starting one up.</p>
|
<p>No chats found! Try starting one up.</p>
|
||||||
<Button class="btn btn-primary" on:click={startChat}>
|
<Button class="btn btn-primary" on:click={startChat}>
|
||||||
<Icon icon="add-circle" />
|
<Icon icon="add-circle" />
|
||||||
Start a Chat
|
Start a Chat
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</ContentSearch>
|
</ContentSearch>
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
|
|
||||||
const assertNotNil = <T,>(x: T | undefined) => x!
|
const assertNotNil = <T,>(x: T | undefined) => x!
|
||||||
|
|
||||||
const showMembers = () => pushModal(ProfileList, {pubkeys: others})
|
const showMembers = () =>
|
||||||
|
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
|
||||||
|
|
||||||
const onSubmit = async ({content, ...params}: EventContent) => {
|
const onSubmit = async ({content, ...params}: EventContent) => {
|
||||||
const tags = [...params.tags, ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
const tags = [...params.tags, ...remove($pubkey!, pubkeys).map(tagPubkey)]
|
||||||
|
|||||||
@@ -31,10 +31,15 @@
|
|||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<ContentSearch>
|
<ContentSearch>
|
||||||
<label slot="input" class="input input-bordered row-2">
|
<label slot="input" class="row-2 input input-bordered">
|
||||||
<Icon icon="magnifer" />
|
<Icon icon="magnifer" />
|
||||||
<!-- svelte-ignore a11y-autofocus -->
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
<input autofocus bind:value={term} class="grow" type="text" placeholder="Search for people..." />
|
<input
|
||||||
|
autofocus
|
||||||
|
bind:value={term}
|
||||||
|
class="grow"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search for people..." />
|
||||||
</label>
|
</label>
|
||||||
<div slot="content" class="col-2" bind:this={element}>
|
<div slot="content" class="col-2" bind:this={element}>
|
||||||
{#each pubkeys.slice(0, limit) as pubkey (pubkey)}
|
{#each pubkeys.slice(0, limit) as pubkey (pubkey)}
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {onMount} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
import {load} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Page from "@lib/components/Page.svelte"
|
import Page from "@lib/components/Page.svelte"
|
||||||
import Delay from "@lib/components/Delay.svelte"
|
import Delay from "@lib/components/Delay.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||||
import {decodeRelay} from "@app/state"
|
import {decodeRelay, MEMBERSHIPS} from "@app/state"
|
||||||
import {pushDrawer} from "@app/modal"
|
import {pushDrawer} from "@app/modal"
|
||||||
|
|
||||||
const openMenu = () => pushDrawer(MenuSpace, {url})
|
const openMenu = () => pushDrawer(MenuSpace, {url})
|
||||||
|
|
||||||
$: url = decodeRelay($page.params.relay)
|
$: url = decodeRelay($page.params.relay)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
load({
|
||||||
|
filters: [{kinds: [MEMBERSHIPS], "#r": [url]}],
|
||||||
|
relays: [url],
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key url}
|
{#key url}
|
||||||
|
|||||||
Reference in New Issue
Block a user