Pull group meta directly from events

This commit is contained in:
Jon Staab 2024-06-18 17:20:12 -07:00
parent 3e3d8ab618
commit c80988882b
26 changed files with 268 additions and 307 deletions

View File

@ -1,15 +1,15 @@
<script lang="ts">
import NoteContentKind1 from "src/app/shared/NoteContentKind1.svelte"
import {groups} from "src/engine"
import {deriveGroupMeta} from "src/engine"
export let address
export let truncate = true
const group = groups.key(address)
const meta = deriveGroupMeta(address)
</script>
<NoteContentKind1
note={{content: $group?.meta?.about || ""}}
note={{content: $meta?.about || ""}}
minLength={100}
maxLength={140}
showEntire={!truncate} />

View File

@ -1,15 +1,15 @@
<script lang="ts">
import ImageCircle from "src/partials/ImageCircle.svelte"
import PlaceholderCircle from "src/app/shared/PlaceholderCircle.svelte"
import {groups} from "src/engine"
import {deriveGroupMeta} from "src/engine"
export let address
const group = groups.key(address)
const meta = deriveGroupMeta(address)
</script>
{#if $group?.meta?.picture}
<ImageCircle src={$group.meta.picture} class={$$props.class} />
{#if $meta?.image}
<ImageCircle src={$meta.image} class={$$props.class} />
{:else}
<PlaceholderCircle pubkey={address} class={$$props.class} />
{/if}

View File

@ -1,24 +1,7 @@
<script context="module" lang="ts">
export type Values = {
id?: string
type: string
feeds: string[][]
relays: string[]
members?: string[]
list_publicly: boolean
meta: {
name: string
about: string
picture: string
banner: string
}
}
</script>
<script lang="ts">
import {join, uniqBy} from "ramda"
import {ucFirst} from "hurdak"
import {Address} from "@welshman/util"
import {Address, GROUP, COMMUNITY} from "@welshman/util"
import {toSpliced} from "src/util/misc"
import {fly} from "src/util/transition"
import {formCtrl} from "src/partials/utils"
@ -35,11 +18,12 @@
import FlexColumn from "src/partials/FlexColumn.svelte"
import Heading from "src/partials/Heading.svelte"
import PersonSelect from "src/app/shared/PersonSelect.svelte"
import type {GroupMeta} from "src/domain"
import {normalizeRelayUrl} from "src/domain"
import {env, hints, relaySearch, feedSearch} from "src/engine"
export let onSubmit
export let values: Values
export let values: GroupMeta & {members: string[]}
export let mode = "create"
export let showMembers = false
export let buttonText = "Save"
@ -83,7 +67,7 @@
<div class="mb-4 flex flex-col items-center justify-center">
<Heading>{ucFirst(mode)} Group</Heading>
<p>
{#if values.type === "open"}
{#if values.kind === COMMUNITY}
An open forum where anyone can participate.
{:else}
A private place where members can talk.
@ -92,30 +76,30 @@
</div>
<div class="flex w-full flex-col gap-8">
<Field label="Name">
<Input bind:value={values.meta.name}>
<Input bind:value={values.name}>
<i slot="before" class="fa fa-clipboard" />
</Input>
<div slot="info">The name of the group</div>
</Field>
<Field label="Picture">
<ImageInput
bind:value={values.meta.picture}
bind:value={values.image}
icon="image-portrait"
maxWidth={480}
maxHeight={480} />
<div slot="info">A picture for the group</div>
</Field>
<Field label="Banner">
<ImageInput bind:value={values.meta.banner} icon="image" maxWidth={4000} maxHeight={4000} />
<ImageInput bind:value={values.banner} icon="image" maxWidth={4000} maxHeight={4000} />
<div slot="info">A banner image for the group</div>
</Field>
<Field label="About">
<Textarea bind:value={values.meta.about} />
<Textarea bind:value={values.about} />
<div slot="info">The group's decription</div>
</Field>
{#if values.type === "closed"}
{#if values.kind === GROUP}
<FieldInline label="List Publicly">
<Toggle bind:value={values.list_publicly} />
<Toggle bind:value={values.listing_is_public} />
<div slot="info">
If enabled, this will generate a public listing for the group. The member list and group
messages will not be published.

View File

@ -1,11 +1,12 @@
<script lang="ts">
import Anchor from "src/partials/Anchor.svelte"
import {router} from "src/app/util/router"
import {deriveGroup, displayGroup, loadGroups} from "src/engine"
import {displayGroupMeta} from "src/domain"
import {deriveGroupMeta, loadGroups} from "src/engine"
export let address
const group = deriveGroup(address)
const meta = deriveGroupMeta(address)
const path = router.at("groups").of(address).at("notes").toString()
loadGroups([address])
@ -13,6 +14,6 @@
<span class={$$props.class}>
<Anchor modal underline href={path}>
{displayGroup($group)}
{displayGroupMeta($meta)}
</Anchor>
</span>

View File

@ -2,17 +2,19 @@
import {ellipsize} from "hurdak"
import {derived} from "svelte/store"
import {remove, intersection} from "@welshman/lib"
import {isGroupAddress, isCommunityAddress} from "@welshman/util"
import Chip from "src/partials/Chip.svelte"
import Card from "src/partials/Card.svelte"
import GroupCircle from "src/app/shared/GroupCircle.svelte"
import PersonCircles from "src/app/shared/PersonCircles.svelte"
import {router} from "src/app/util/router"
import {displayGroup, deriveGroup, userFollows, communityListsByAddress, pubkey} from "src/engine"
import {displayGroupMeta} from "src/domain"
import {deriveGroupMeta, userFollows, communityListsByAddress, pubkey} from "src/engine"
export let address
export let modal = false
const group = deriveGroup(address)
const meta = deriveGroupMeta(address)
const members = derived(communityListsByAddress, $m => {
const allMembers = $m.get(address)?.map(l => l.event.pubkey) || []
const otherMembers = remove($pubkey, allMembers)
@ -37,20 +39,20 @@
<div class="flex min-w-0 flex-grow flex-col justify-start gap-1">
<div class="flex justify-between gap-2">
<h2 class="text-xl font-bold">
{displayGroup($group)}
{displayGroupMeta($meta)}
</h2>
<slot name="actions">
{#if address.startsWith("34550:")}
{#if isCommunityAddress(address)}
<Chip class="text-sm text-neutral-200"><i class="fa fa-unlock" /> Open</Chip>
{/if}
{#if address.startsWith("35834:")}
{#if isGroupAddress(address)}
<Chip class="text-sm text-neutral-200"><i class="fa fa-lock" /> Closed</Chip>
{/if}
</slot>
</div>
{#if $group.meta?.about}
{#if $meta?.about}
<p class="text-start text-neutral-100">
{ellipsize($group.meta.about, 300)}
{ellipsize($meta.about, 300)}
</p>
{/if}
{#if $members.length > 0}

View File

@ -1,9 +1,10 @@
<script lang="ts">
import {groups, displayGroup} from "src/engine"
import {displayGroupMeta} from "src/domain"
import {deriveGroupMeta} from "src/engine"
export let address
const group = groups.key(address)
const meta = deriveGroupMeta(address)
</script>
<span class={$$props.class}>{displayGroup($group)}</span>
<span class={$$props.class}>{displayGroupMeta($meta)}</span>

View File

@ -9,12 +9,11 @@
import Feed from "src/app/shared/Feed.svelte"
import NoteCreateInline from "src/app/shared/NoteCreateInline.svelte"
import {makeFeed, readFeed} from "src/domain"
import {hints, repository, canSign, deriveGroup, load} from "src/engine"
import {hints, repository, canSign, deriveGroupMeta, load} from "src/engine"
export let address
const group = deriveGroup(address)
const meta = deriveGroupMeta(address)
const mainFeed = feedFromFilter({kinds: remove(30402, noteKinds), "#a": [address]})
const setActiveTab = tab => {
@ -27,7 +26,7 @@
let feeds = [{name: "feed", feed: makeFeed({definition: mainFeed})}]
let feed = makeFeed({definition: mainFeed})
for (const feed of $group.feeds || []) {
for (const feed of $meta?.feeds || []) {
const [address, relay = "", name = ""] = feed.slice(1)
if (!Address.isAddress(address)) {

View File

@ -1,14 +1,15 @@
<script lang="ts">
import {isCommunityAddress} from "@welshman/util"
import Chip from "src/partials/Chip.svelte"
import GroupCircle from "src/app/shared/GroupCircle.svelte"
import GroupAbout from "src/app/shared/GroupAbout.svelte"
import GroupName from "src/app/shared/GroupName.svelte"
import {groups} from "src/engine"
import {deriveGroupMeta} from "src/engine"
export let address
export let hideAbout = false
const group = groups.key(address)
const meta = deriveGroupMeta(address)
</script>
<div class="flex gap-4 text-neutral-100">
@ -18,7 +19,7 @@
<div class="flex items-center">
<GroupName class="text-2xl" {address} />
<Chip class="scale-75 border-neutral-200 text-neutral-200">
{#if address.startsWith("34550:")}
{#if isCommunityAddress(address)}
<i class="fa fa-unlock" />
Open
{:else}
@ -29,7 +30,7 @@
</div>
<slot name="actions" class="hidden xs:block" />
</div>
{#if !hideAbout && $group?.meta?.about}
{#if !hideAbout && $meta?.about}
<GroupAbout {address} />
{/if}
</div>

View File

@ -1,9 +1,11 @@
<script lang="ts">
import {nth} from "@welshman/lib"
import {COMMUNITY, GROUP} from "@welshman/util"
import Card from "src/partials/Card.svelte"
import Heading from "src/partials/Heading.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import type {Values} from "src/app/shared/GroupDetailsForm.svelte"
import GroupDetailsForm from "src/app/shared/GroupDetailsForm.svelte"
import type {GroupMeta} from "src/domain"
import {
env,
pubkey,
@ -19,34 +21,38 @@
import {router} from "src/app/util/router"
const initialValues = {
type: null,
kind: null,
name: "",
about: "",
image: "",
banner: "",
feeds: [],
relays: $env.PLATFORM_RELAYS,
relays: $env.PLATFORM_RELAYS.map(url => ["relay", url]),
listing_is_public: false,
members: [$pubkey],
list_publicly: false,
meta: {
name: "",
about: "",
picture: "",
banner: "",
},
moderators: [],
identifier: "",
}
const setType = type => {
initialValues.type = type
const setKind = kind => {
initialValues.kind = kind
}
const onSubmit = async ({type, feeds, relays, members, list_publicly, meta}: Values) => {
const kind = type === "open" ? 34550 : 35834
const {id, address} = initGroup(kind, relays)
const onSubmit = async ({
kind,
members,
listing_is_public,
...meta
}: GroupMeta & {members: string[]}) => {
const {identifier, address} = initGroup(kind, meta.relays.map(nth(1)))
await publishAdminKeyShares(address, [$pubkey])
if (type === "open") {
await publishCommunityMeta(address, id, feeds, relays, meta)
if (kind === COMMUNITY) {
await publishCommunityMeta(address, identifier, meta)
await publishCommunitiesList(deriveUserCommunities().get().concat(address))
} else {
await publishGroupMeta(address, id, feeds, relays, meta, list_publicly)
await publishGroupMeta(address, identifier, meta, listing_is_public)
await publishGroupMembers(address, "set", members)
await publishGroupInvites(address, members)
}
@ -55,12 +61,12 @@
}
</script>
{#if !initialValues.type}
{#if !initialValues.kind}
<div class="mb-4 flex flex-col items-center justify-center">
<Heading>Create Group</Heading>
<p>What type of group would you like to create?</p>
</div>
<Card interactive on:click={() => setType("open")}>
<Card interactive on:click={() => setKind(COMMUNITY)}>
<FlexColumn>
<div class="flex items-center justify-between">
<p class="flex items-center gap-4 text-xl">
@ -75,7 +81,7 @@
</p>
</FlexColumn>
</Card>
<Card interactive on:click={() => setType("closed")}>
<Card interactive on:click={() => setKind(GROUP)}>
<FlexColumn>
<div class="flex items-center justify-between">
<p class="flex items-center gap-4 text-xl">
@ -94,11 +100,11 @@
<div class="relative">
<i
class="fa fa-2x fa-arrow-left absolute top-12 cursor-pointer"
on:click={() => setType(null)} />
on:click={() => setKind(null)} />
<GroupDetailsForm
{onSubmit}
values={initialValues}
showMembers={initialValues.type === "closed"}
buttonText={`Create ${initialValues.type} group`} />
showMembers={initialValues.kind === GROUP}
buttonText={initialValues.kind === GROUP ? "Create closed group" : "Create open group"} />
</div>
{/if}

View File

@ -2,12 +2,13 @@
import {showInfo} from "src/partials/Toast.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Subheading from "src/partials/Subheading.svelte"
import {groups, createAndPublish, hints, deriveAdminKeyForGroup, displayGroup} from "src/engine"
import {displayGroupMeta} from "src/domain"
import {deriveGroupMeta, createAndPublish, hints, deriveAdminKeyForGroup} from "src/engine"
import {router} from "src/app/util/router"
export let address
const group = groups.key(address)
const meta = deriveGroupMeta(address)
const adminKey = deriveAdminKeyForGroup(address)
const abort = () => router.pop()
@ -31,7 +32,7 @@
</script>
<Subheading>Delete Group</Subheading>
<p>Are you sure you want to delete {displayGroup($group)}?</p>
<p>Are you sure you want to delete {displayGroupMeta($meta)}?</p>
<p>
This will only hide this group from supporting clients. Messages sent to the group may not be
deleted from relays.

View File

@ -13,13 +13,14 @@
import GroupMembers from "src/app/shared/GroupMembers.svelte"
import GroupAdmin from "src/app/shared/GroupAdmin.svelte"
import GroupRestrictAccess from "src/app/shared/GroupRestrictAccess.svelte"
import {displayGroupMeta} from "src/domain"
import {
env,
GroupAccess,
displayGroup,
loadPubkeys,
groupRequests,
deriveGroup,
deriveGroupMeta,
deriveAdminKeyForGroup,
deriveSharedKeyForGroup,
deriveIsGroupMember,
@ -35,6 +36,7 @@
export let claim = ""
const group = deriveGroup(address)
const meta = deriveGroupMeta(address)
const status = deriveGroupStatus(address)
const isGroupMember = deriveIsGroupMember(address)
const sharedKey = deriveSharedKeyForGroup(address)
@ -86,21 +88,21 @@
$: ({rgb, rgba} = $themeBackgroundGradient)
document.title = $group?.meta?.name || "Group Detail"
document.title = $meta?.name || "Group Detail"
</script>
<div
class="absolute left-0 top-0 h-64 w-full"
style={`z-index: -1;
background-size: cover;
background-image: linear-gradient(to bottom, ${rgba}, ${rgb}), url('${$group?.meta?.banner}')`} />
background-image: linear-gradient(to bottom, ${rgba}, ${rgb}), url('${$meta?.banner}')`} />
<div class="flex gap-4 text-neutral-100">
<GroupCircle {address} class="mt-1 h-10 w-10 sm:h-32 sm:w-32" />
<div class="flex min-w-0 flex-grow flex-col gap-4">
<div class="flex items-center justify-between gap-4">
<Anchor on:click={() => setActiveTab("notes")} class="text-2xl"
>{displayGroup($group)}</Anchor>
>{displayGroupMeta($meta)}</Anchor>
<GroupActions {address} {claim} />
</div>
<GroupAbout {address} />

View File

@ -1,46 +1,39 @@
<script lang="ts">
import {prop} from "ramda"
import {COMMUNITY} from "@welshman/util"
import {showInfo} from "src/partials/Toast.svelte"
import type {Values} from "src/app/shared/GroupDetailsForm.svelte"
import GroupDetailsForm from "src/app/shared/GroupDetailsForm.svelte"
import type {GroupMeta} from "src/domain"
import {
deriveGroup,
deleteGroupMeta,
publishGroupMeta,
publishCommunityMeta,
getGroupId,
getGroupName,
deriveGroup,
deriveGroupMeta,
} from "src/engine"
import {router} from "src/app/util/router"
export let address
const group = deriveGroup(address)
const meta = deriveGroupMeta(address)
const initialValues = {...$meta, members: $group?.members || []}
const initialValues = {
id: getGroupId($group),
type: address.startsWith("34550:") ? "open" : "closed",
feeds: $group.feeds || [],
relays: $group.relays || [],
list_publicly: $group.listing_is_public,
meta: {
name: getGroupName($group),
about: $group.meta?.about || "",
picture: $group.meta?.picture || "",
banner: $group.meta?.banner || "",
},
}
const onSubmit = async ({id, type, list_publicly, feeds, relays, meta}: Values) => {
const onSubmit = async ({
kind,
identifier,
listing_is_public,
...meta
}: GroupMeta & {members: string[]}) => {
// If we're switching group listing visibility, delete the old listing
if ($group.listing_is_public && !list_publicly) {
await prop("result", await deleteGroupMeta($group.address))
if (listing_is_public && !initialValues.listing_is_public) {
await prop("result", await deleteGroupMeta(address))
}
if (type === "open") {
await publishCommunityMeta(address, id, feeds, relays, meta)
if (kind === COMMUNITY) {
await publishCommunityMeta(address, identifier, meta)
} else {
await publishGroupMeta(address, id, feeds, relays, meta, list_publicly)
await publishGroupMeta(address, identifier, meta, listing_is_public)
}
showInfo("Your group has been updated!")

View File

@ -1,18 +1,20 @@
<script lang="ts">
import {toNostrURI} from "@welshman/util"
import {nth} from "@welshman/lib"
import {toNostrURI, Address} from "@welshman/util"
import {nsecEncode} from "src/util/nostr"
import Anchor from "src/partials/Anchor.svelte"
import Popover from "src/partials/Popover.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
import CopyValue from "src/partials/CopyValue.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import {groups, deriveAdminKeyForGroup, getGroupNaddr} from "src/engine"
import {deriveGroupMeta, deriveAdminKeyForGroup} from "src/engine"
import {router} from "src/app/util/router"
export let address
const group = groups.key(address)
const meta = deriveGroupMeta(address)
const adminKey = deriveAdminKeyForGroup(address)
const naddr = Address.from(address, $meta?.relays.map(nth(1)) || []).toNaddr()
const shareAdminKey = () => {
popover?.hide()
@ -24,7 +26,7 @@
<h1 class="staatliches text-2xl">Details</h1>
<CopyValue label="Group ID" value={address} />
<CopyValue label="Link" value={toNostrURI(getGroupNaddr($group))} />
<CopyValue label="Link" value={toNostrURI(naddr)} />
{#if $adminKey}
<CopyValue isPassword label="Admin key" value={$adminKey.privkey} encode={nsecEncode}>
<div slot="label" class="flex gap-2">
@ -46,12 +48,12 @@
</div>
</CopyValue>
{/if}
{#if $group.relays?.length > 0}
{#if $meta?.relays.length > 0}
<h1 class="staatliches text-2xl">Relays</h1>
<p>This group uses the following relays:</p>
<div class="flex flex-col gap-2">
{#each $group.relays as url}
<RelayCard {url} />
{#each $meta.relays as tag}
<RelayCard url={tag[1]} />
{/each}
</div>
{/if}

View File

@ -1,8 +1,9 @@
<script>
import {onMount} from "svelte"
import {filter, assoc} from "ramda"
import {filter, reject, assoc} from "ramda"
import {derived} from "svelte/store"
import {now, shuffle} from "@welshman/lib"
import {GROUP, COMMUNITY, getIdFilters} from "@welshman/util"
import {GROUP, COMMUNITY, getAddress, getIdFilters} from "@welshman/util"
import {createScroller} from "src/util/misc"
import Anchor from "src/partials/Anchor.svelte"
import FlexColumn from "src/partials/FlexColumn.svelte"
@ -12,40 +13,37 @@
load,
hints,
groups,
repository,
loadGiftWraps,
loadGroupMessages,
deriveIsGroupMember,
updateCurrentSession,
communityListsByAddress,
searchGroups,
searchGroupMeta,
groupMeta,
} from "src/engine"
const loadMore = async () => {
limit += 20
}
const userIsMember = g => deriveIsGroupMember(g.address, true).get()
const userIsMember = meta => deriveIsGroupMember(getAddress(meta.event), true).get()
const userGroups = groups.derived(
filter(g => !repository.deletes.has(g.address) && userIsMember(g)),
)
const userGroupMeta = derived(groupMeta, filter(userIsMember))
let q = ""
let limit = 20
let element = null
$: otherGroups = $searchGroups(q)
.filter(g => !userIsMember(g))
.slice(0, limit)
$: otherGroupMeta = reject(userIsMember, $searchGroupMeta(q)).slice(0, limit)
document.title = "Groups"
onMount(() => {
const loader = loadGiftWraps()
const scroller = createScroller(loadMore, {element})
const communityAddrs = Array.from($communityListsByAddress.keys())
.filter(a => !groups.key(a).get()?.meta)
const communityAddrs = Array.from($communityListsByAddress.keys()).filter(
a => !groups.key(a).get()?.meta,
)
updateCurrentSession(assoc("groups_last_synced", now()))
@ -81,8 +79,8 @@
<i class="fa-solid fa-plus" /> Create
</Anchor>
</div>
{#each $userGroups as group (group.address)}
<GroupListItem address={group.address} />
{#each $userGroupMeta as meta (meta.event.id)}
<GroupListItem address={getAddress(meta.event)} />
{:else}
<p class="text-center py-8">You haven't yet joined any groups.</p>
{/each}
@ -90,7 +88,7 @@
<Input bind:value={q} type="text" class="flex-grow" placeholder="Search groups">
<i slot="before" class="fa-solid fa-search" />
</Input>
{#each otherGroups as group (group.address)}
<GroupListItem address={group.address} />
{#each otherGroupMeta as meta (meta.event.id)}
<GroupListItem address={getAddress(meta.event)} />
{/each}
</FlexColumn>

View File

@ -1,5 +1,5 @@
<script lang="ts">
import {without} from "ramda"
import {without} from "@welshman/lib"
import {difference} from "hurdak"
import {showInfo} from "src/partials/Toast.svelte"
import Field from "src/partials/Field.svelte"
@ -11,9 +11,11 @@
import PersonSelect from "src/app/shared/PersonSelect.svelte"
import type {GroupRequest} from "src/engine"
import {
hints,
groups,
groupRequests,
initSharedKey,
deriveGroupMeta,
deriveSharedKeyForGroup,
publishGroupInvites,
publishGroupEvictions,
@ -27,6 +29,7 @@
export let removeMembers = []
const group = groups.key(address)
const meta = deriveGroupMeta(address)
const sharedKey = deriveSharedKeyForGroup(address)
const initialMembers = new Set(
without(removeMembers, [...($group?.members || []), ...addMembers]),
@ -34,7 +37,7 @@
const onSubmit = () => {
if (!soft || !$sharedKey) {
initSharedKey(address)
initSharedKey(address, hints.WithinContext(address).getUrls())
}
const allMembers = new Set(members)
@ -81,8 +84,8 @@
}
// Re-publish group info
if (!soft && !$group.listing_is_public) {
publishGroupMeta(address, $group.id, $group.feeds, $group.relays, $group.meta, false)
if (!soft && !$meta.listing_is_public) {
publishGroupMeta(address, $meta.identifier, $meta, false)
}
// Re-send invites. This could be optimized further, but it's useful to re-send to different relays.

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {zipObj, uniq, pluck} from "ramda"
import {normalizeRelayUrl, Address} from "@welshman/util"
import {zipObj, pluck} from "ramda"
import {normalizeRelayUrl} from "@welshman/util"
import {updateIn} from "src/util/misc"
import Card from "src/partials/Card.svelte"
import Heading from "src/partials/Heading.svelte"
@ -12,7 +12,7 @@
import GroupActions from "src/app/shared/GroupActions.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import Onboarding from "src/app/views/Onboarding.svelte"
import {session, loadGroups, groups as allGroups} from "src/engine"
import {session, loadGroups, groupHints} from "src/engine"
export let people = []
export let relays = []
@ -30,24 +30,9 @@
loadGroups(pluck("address", parsedGroups) as string[], pluck("relay", parsedGroups) as string[])
// Add relay hints to groups so we can use them deep in the call stack
for (const {address, relay} of parsedGroups) {
const group = allGroups.key(address)
if (relay) {
const {identifier, pubkey} = Address.from(address)
group.update($g => {
const {relays = []} = $g
return {
...$g,
address,
id: identifier,
pubkey: pubkey,
relays: uniq([...relays, relay]),
}
})
groupHints.update($gh => ({...$gh, [address]: [...$gh[address], relay]}))
}
}
</script>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {prop} from "ramda"
import {without, identity} from "@welshman/lib"
import {getAddress} from "@welshman/util"
import {onMount} from "svelte"
import {pickVals, toSpliced} from "src/util/misc"
import Card from "src/partials/Card.svelte"
@ -15,8 +15,8 @@
import GroupCircle from "src/app/shared/GroupCircle.svelte"
import PersonSelect from "src/app/shared/PersonSelect.svelte"
import {router} from "src/app/util/router"
import {displayRelayUrl} from "src/domain"
import {hints, relaySearch, searchGroups, displayGroup, deriveGroup} from "src/engine"
import {displayRelayUrl, displayGroupMeta} from "src/domain"
import {hints, relaySearch, searchGroupMeta, groupMetaByAddress} from "src/engine"
export let initialPubkey = null
export let initialGroupAddress = null
@ -70,7 +70,7 @@
groups = toSpliced(groups, i, 1)
}
const displayGroupFromAddress = a => displayGroup(deriveGroup(a).get())
const displayGroupFromAddress = a => displayGroupMeta($groupMetaByAddress.get(a))
let relayInput, groupInput
let sections = []
@ -194,14 +194,14 @@
<SearchSelect
value={null}
bind:this={groupInput}
search={$searchGroups}
getKey={prop("address")}
onChange={g => g && addGroup(g.address)}
displayItem={g => (g ? displayGroup(g) : "")}>
search={$searchGroupMeta}
displayItem={displayGroupMeta}
getKey={groupMeta => getAddress(groupMeta.event)}
onChange={groupMeta => groupMeta && addGroup(getAddress(groupMeta.event))}>
<i slot="before" class="fa fa-search" />
<div slot="item" let:item class="flex items-center gap-4 text-neutral-100">
<GroupCircle address={item.address} class="h-5 w-5" />
<GroupName address={item.address} />
<GroupCircle address={getAddress(item.event)} class="h-5 w-5" />
<GroupName address={getAddress(item.event)} />
</div>
</SearchSelect>
</FlexColumn>

41
src/domain/group.ts Normal file
View File

@ -0,0 +1,41 @@
import {fromPairs, nthEq} from '@welshman/lib'
import type {TrustedEvent} from "@welshman/util"
import {isSignedEvent} from "@welshman/util"
export type GroupMeta = {
kind: number
feeds: string[][]
relays: string[][]
moderators: string[][]
identifier: string
name: string
about: string
banner: string
image: string
listing_is_public: boolean
event?: TrustedEvent
}
export type PublishedGroupMeta = Omit<GroupMeta, "event"> & {
event: TrustedEvent
}
export const readGroupMeta = (event: TrustedEvent) => {
const meta = fromPairs(event.tags)
return {
event,
kind: event.kind,
feeds: event.tags.filter(nthEq(0, 'feed')),
relays: event.tags.filter(nthEq(0, 'relay')),
moderators: event.tags.filter(nthEq(0, 'p')),
identifier: meta.d,
name: meta.name,
about: meta.about,
banner: meta.banner,
image: meta.image || meta.picture,
listing_is_public: isSignedEvent(event),
} as PublishedGroupMeta
}
export const displayGroupMeta = (meta: GroupMeta) => meta?.name || meta?.identifier || "[no name]"

View File

@ -1,5 +1,6 @@
export * from "./collection"
export * from "./feed"
export * from "./group"
export * from "./handle"
export * from "./handler"
export * from "./kind"

View File

@ -51,7 +51,6 @@ import {
loadOne,
createAndPublish,
deriveAdminKeyForGroup,
deriveGroup,
deriveIsGroupMember,
deriveSharedKeyForGroup,
displayProfileByPubkey,
@ -250,7 +249,7 @@ export const updateZapper = async ({pubkey, created_at}, {lud16, lud06}) => {
// Key state management
export const initSharedKey = address => {
export const initSharedKey = (address: string, relays: string[]) => {
const privkey = generatePrivateKey()
const pubkey = getPublicKey(privkey)
const key = {
@ -258,6 +257,7 @@ export const initSharedKey = address => {
pubkey: pubkey,
privkey: privkey,
created_at: now(),
hints: relays,
}
groupSharedKeys.key(pubkey).set(key)
@ -266,24 +266,24 @@ export const initSharedKey = address => {
}
export const initGroup = (kind, relays) => {
const id = randomId()
const identifier = randomId()
const privkey = generatePrivateKey()
const pubkey = getPublicKey(privkey)
const address = `${kind}:${pubkey}:${id}`
const sharedKey = kind === 35834 ? initSharedKey(address) : null
const address = `${kind}:${pubkey}:${identifier}`
const sharedKey = kind === 35834 ? initSharedKey(address, relays) : null
const adminKey = {
group: address,
pubkey: pubkey,
privkey: privkey,
created_at: now(),
relays,
hints: relays,
}
groupAdminKeys.key(pubkey).set(adminKey)
groups.key(address).set({id, pubkey, address, relays})
groups.key(address).set({id: identifier, pubkey, address})
return {id, address, adminKey, sharedKey}
return {identifier, address, adminKey, sharedKey}
}
// Most people don't have access to nip44 yet, send nip04-encrypted fallbacks for:
@ -485,7 +485,7 @@ export const publishKeyShares = async (address, pubkeys, template) => {
}
export const publishAdminKeyShares = async (address, pubkeys) => {
const {relays} = deriveGroup(address).get()
const relays = hints.WithinContext(address).getUrls()
const {privkey} = deriveAdminKeyForGroup(address).get()
const template = createEvent(24, {
tags: [
@ -501,7 +501,7 @@ export const publishAdminKeyShares = async (address, pubkeys) => {
}
export const publishGroupInvites = async (address, pubkeys, gracePeriod = 0) => {
const {relays} = deriveGroup(address).get()
const relays = hints.WithinContext(address).getUrls()
const adminKey = deriveAdminKeyForGroup(address).get()
const {privkey} = deriveSharedKeyForGroup(address).get()
const template = createEvent(24, {
@ -535,40 +535,44 @@ export const publishGroupMembers = async (address, op, pubkeys) => {
return publishAsGroupAdminPrivately(address, template)
}
export const publishCommunityMeta = (address, id, feeds, relays, meta) => {
export const publishCommunityMeta = (address, identifier, meta) => {
const template = createEvent(34550, {
tags: [
["d", id],
["d", identifier],
["name", meta.name],
["description", meta.about],
["banner", meta.banner],
["image", meta.picture],
["picture", meta.image],
["image", meta.image],
...meta.feeds,
...meta.relays,
...meta.moderators,
...getClientTags(),
...(feeds || []),
...(relays || []).map(url => ["relay", url]),
],
})
return publishAsGroupAdminPublicly(address, template, relays)
return publishAsGroupAdminPublicly(address, template, meta.relays)
}
export const publishGroupMeta = (address, id, feeds, relays, meta, listPublicly) => {
export const publishGroupMeta = (address, identifier, meta, listPublicly) => {
const template = createEvent(35834, {
tags: [
["d", id],
["d", identifier],
["name", meta.name],
["about", meta.about],
["banner", meta.banner],
["picture", meta.picture],
["picture", meta.image],
["image", meta.image],
...meta.feeds,
...meta.relays,
...meta.moderators,
...getClientTags(),
...(feeds || []),
...(relays || []).map(url => ["relay", url]),
],
})
return listPublicly
? publishAsGroupAdminPublicly(address, template, relays)
: publishAsGroupAdminPrivately(address, template, relays)
? publishAsGroupAdminPublicly(address, template, meta.relays)
: publishAsGroupAdminPrivately(address, template, meta.relays)
}
export const deleteGroupMeta = address =>

View File

@ -22,27 +22,12 @@ export enum GroupAccess {
Revoked = "revoked",
}
export type GroupMeta = {
name?: string
about?: string
banner?: string
picture?: string
}
export type Group = {
id: string
pubkey: string
address: string
meta?: GroupMeta
meta_updated_at?: number
feeds?: string[][]
feeds_updated_at?: number
relays?: string[]
relays_updated_at?: number
members?: string[]
recent_member_updates?: TrustedEvent[]
listing_is_public?: boolean
listing_is_public_updated?: boolean
}
export type GroupKey = {

View File

@ -5,8 +5,6 @@ import type {TrustedEvent} from "@welshman/util"
import {
Tags,
isShareableRelayUrl,
Address,
getAddress,
getIdFilters,
MUTES,
FOLLOWS,
@ -43,7 +41,6 @@ import {
modifyGroupStatus,
setGroupStatus,
updateRecord,
updateStore,
updateSession,
setSession,
updateZapper,
@ -109,63 +106,11 @@ projections.addHandler(24, (e: TrustedEvent) => {
groupAlerts.key(e.id).set({...e, group: address, type: "exit"})
}
if (relays.length > 0) {
const {pubkey, identifier} = Address.from(address)
if (!groups.key(address).get()) {
groups.key(address).set({address, pubkey, id: identifier, relays})
}
}
setGroupStatus(recipient, address, e.created_at, {
access: privkey ? GroupAccess.Granted : GroupAccess.Revoked,
})
})
// Group metadata
projections.addHandler(35834, (e: TrustedEvent) => {
const tags = Tags.fromEvent(e)
const meta = tags.asObject()
const address = getAddress(e)
const group = groups.key(address)
group.merge({address, id: meta.d, pubkey: e.pubkey})
updateStore(group, e.created_at, {
feeds: tags.whereKey("feed").unwrap(),
relays: tags.values("relay").valueOf(),
listing_is_public: !e.wrap,
meta: {
name: meta.name,
about: meta.about,
banner: meta.banner,
picture: meta.picture,
},
})
})
projections.addHandler(34550, (e: TrustedEvent) => {
const tags = Tags.fromEvent(e)
const meta = tags.asObject()
const address = getAddress(e)
const group = groups.key(address)
group.merge({address, id: meta.d, pubkey: e.pubkey})
updateStore(group, e.created_at, {
feeds: tags.whereKey("feed").unwrap(),
relays: tags.values("relay").valueOf(),
listing_is_public: true,
meta: {
name: meta.name,
about: meta.description,
banner: meta.banner,
picture: meta.image,
},
})
})
projections.addHandler(27, (e: TrustedEvent) => {
const address = Tags.fromEvent(e).groups().values().first()

View File

@ -1,5 +1,6 @@
import {debounce} from "throttle-debounce"
import {batch, Fetch, noop, tryFunc, seconds, createMapOf, sleep, switcherFn} from "hurdak"
import {get} from "svelte/store"
import type {LoadOpts} from "@welshman/feeds"
import {
FeedLoader,
@ -38,7 +39,6 @@ import {
deriveUserCircles,
getGroupReqInfo,
getCommunityReqInfo,
groups,
dvmRequest,
env,
getFollows,
@ -60,6 +60,7 @@ import {
subscribe,
subscribePersistent,
dufflepud,
deriveGroupMeta,
} from "src/engine/state"
import {updateCurrentSession, updateSession} from "src/engine/commands"
import {loadPubkeyRelays} from "src/engine/requests/pubkeys"
@ -140,9 +141,14 @@ export const getStaleAddrs = (addrs: string[]) => {
for (const addr of addrs) {
const attempts = attemptedAddrs.get(addr) | 0
const group = groups.key(addr).get()
if (!group?.meta || attempts === 0) {
if (attempts > 0) {
continue
}
const meta = get(deriveGroupMeta(addr))
if (!meta) {
stale.add(addr)
}

View File

@ -2,7 +2,7 @@ import Fuse from "fuse.js"
import {openDB, deleteDB} from "idb"
import type {IDBPDatabase} from "idb"
import {throttle} from "throttle-debounce"
import {derived, writable} from "svelte/store"
import {get, derived, writable} from "svelte/store"
import {defer, doPipe, batch, randomInt, seconds, sleep, switcher} from "hurdak"
import {
any,
@ -43,6 +43,8 @@ import {
} from "@welshman/lib"
import {
WRAP,
COMMUNITY,
GROUP,
WRAP_NIP04,
COMMUNITIES,
READ_RECEIPT,
@ -103,6 +105,7 @@ import type {
PublishedListFeed,
PublishedSingleton,
PublishedList,
PublishedGroupMeta,
RelayPolicy,
Handle,
} from "src/domain"
@ -130,6 +133,7 @@ import {
makeRelayPolicy,
filterRelaysByNip,
displayRelayUrl,
readGroupMeta,
} from "src/domain"
import type {
Channel,
@ -178,6 +182,7 @@ export const handles = withGetter(writable<Record<string, Handle>>({}))
export const zappers = withGetter(writable<Record<string, Zapper>>({}))
export const plaintext = withGetter(writable<Record<string, string>>({}))
export const anonymous = withGetter(writable<AnonymousUserState>({follows: [], relays: []}))
export const groupHints = withGetter(writable<Record<string, string[]>>({}))
export const groups = new CollectionStore<Group>("address")
export const relays = new CollectionStore<RelayInfo>("url")
@ -719,46 +724,53 @@ export const deriveCommunities = (pk: string) =>
// Groups
export const deriveGroup = address => {
const {pubkey, identifier: id} = Address.from(address)
export const groupMeta = deriveEventsMapped<PublishedGroupMeta>({
filters: [{kinds: [GROUP, COMMUNITY]}],
itemToEvent: prop("event"),
eventToItem: readGroupMeta,
})
return groups.key(address).derived(defaultTo({id, pubkey, address}))
}
export const groupMetaByAddress = withGetter(
derived(groupMeta, $metas => indexBy($meta => getAddress($meta.event), $metas)),
)
export const searchGroups = derived(
[groups.throttle(300), communityListsByAddress, userFollows],
([$groups, $communityListsByAddress, $userFollows]) => {
const options = $groups
.filter(group => !repository.deletes.has(group.address))
.map(group => {
const lists = $communityListsByAddress.get(group.address) || []
const members = lists.map(l => l.event.pubkey)
const followedMembers = intersection(members, $userFollows)
export const deriveGroupMeta = (address: string) =>
derived(groupMetaByAddress, $m => $m.get(address))
return {group, score: followedMembers.length}
})
export const searchGroupMeta = derived(
[groupMeta, communityListsByAddress, userFollows],
([$groupMeta, $communityListsByAddress, $userFollows]) => {
const options = $groupMeta.map(meta => {
const lists = $communityListsByAddress.get(getAddress(meta.event)) || []
const members = lists.map(l => l.event.pubkey)
const followedMembers = intersection(members, Array.from($userFollows))
return {...meta, score: followedMembers.length}
})
const fuse = new Fuse(options, {
keys: [{name: "group.id", weight: 0.2}, "group.meta.name", "group.meta.about"],
keys: [{name: "identifier", weight: 0.2}, "name", {name: "about", weight: 0.5}],
threshold: 0.3,
shouldSort: false,
includeScore: true,
})
return (term: string) => {
if (!term) {
return sortBy(item => -item.score, options).map(item => item.group)
}
const sortFn = (r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100)
return doPipe(fuse.search(term), [
$results =>
sortBy((r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100), $results),
$results => $results.map((r: any) => r.item.group),
])
}
return (term: string) =>
term
? sortBy(sortFn, fuse.search(term)).map((r: any) => r.item.meta)
: sortBy(meta => -meta.score, options)
},
)
// Legacy
export const deriveGroup = address => {
const {pubkey, identifier: id} = Address.from(address)
return groups.key(address).derived(defaultTo({id, pubkey, address}))
}
export const getRecipientKey = wrap => {
const pubkey = Tags.fromEvent(wrap).values("p").first()
const sharedKey = groupSharedKeys.key(pubkey).get()
@ -1147,20 +1159,21 @@ export const deriveUserRelayPolicy = url =>
// Relay selection
export const getGroupRelayUrls = address => {
const group = groups.key(address).get()
const keys = groupSharedKeys.get()
const meta = groupMetaByAddress.get().get(address)
if (group?.relays) {
return group.relays
if (meta?.relays) {
return meta.relays.map(nth(1))
}
const latestKey = last(sortBy(prop("created_at"), keys.filter(whereEq({group: address}))))
const latestKey = last(
sortBy(prop("created_at"), get(groupSharedKeys).filter(whereEq({group: address}))),
)
if (latestKey?.hints) {
return latestKey.hints
}
return []
return get(groupHints)[address] || []
}
export const forceRelays = (relays: string[], forceRelays: string[]) =>

View File

@ -1,11 +0,0 @@
import {ellipsize} from "hurdak"
import {Address} from "@welshman/util"
import type {Group} from "src/engine/model"
export const getGroupNaddr = (group: Group) => Address.from(group.address, group.relays).toNaddr()
export const getGroupId = (group: Group) => group.address.split(":").slice(2).join(":")
export const getGroupName = (group: Group) => group.meta?.name || group.id || ""
export const displayGroup = (group: Group) => ellipsize(group ? getGroupName(group) : "No name", 60)

View File

@ -12,7 +12,6 @@ export * from "./nip59"
export * from "./signer"
export * from "./connect"
export * from "./events"
export * from "./groups"
export const getConnect = memoize(session => new Connect(session))