mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Compare commits
5 Commits
c15453b996
...
c80988882b
Author | SHA1 | Date | |
---|---|---|---|
|
c80988882b | ||
|
3e3d8ab618 | ||
|
058a99b372 | ||
|
a23f71d307 | ||
|
1dccb3e95c |
@ -7,6 +7,9 @@
|
||||
- [x] Add `k` tag to deletions
|
||||
- [x] Allow users to choose where to publish their profile when using a white-labeled instance
|
||||
- [x] Add "open with" link populated by nip 89 handlers
|
||||
- [x] Fix several community and calendar related bugs
|
||||
- [x] Add reports using tagr-bot
|
||||
- [x] Open links to coracle in same tab
|
||||
|
||||
# 0.4.6
|
||||
|
||||
|
@ -125,7 +125,7 @@
|
||||
class="staatliches px-8 text-tinted-400 hover:text-tinted-100"
|
||||
on:click={() => setSubMenu("settings")}>Settings</Anchor>
|
||||
<div class="staatliches block flex h-8 gap-2 px-8 text-tinted-500">
|
||||
<Anchor external class="hover:text-tinted-100" href="/about">About</Anchor> /
|
||||
<Anchor class="hover:text-tinted-100" href="/about">About</Anchor> /
|
||||
<Anchor external class="hover:text-tinted-100" href="/terms.html">Terms</Anchor> /
|
||||
<Anchor external class="hover:text-tinted-100" href="/privacy.html">Privacy</Anchor>
|
||||
</div>
|
||||
|
@ -1,31 +1,41 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "ramda"
|
||||
import {batch} from "hurdak"
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {writable} from "@welshman/lib"
|
||||
import {onMount} from "svelte"
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import {getAddress, getReplyFilters} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {feedFromFilter} from "@welshman/feeds"
|
||||
import Calendar from "@event-calendar/core"
|
||||
import DayGrid from "@event-calendar/day-grid"
|
||||
import Interaction from "@event-calendar/interaction"
|
||||
import {secondsToDate} from "src/util/misc"
|
||||
import {themeColors} from "src/partials/state"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {
|
||||
hints,
|
||||
load,
|
||||
pubkey,
|
||||
canSign,
|
||||
repository,
|
||||
subscribe,
|
||||
feedLoader,
|
||||
getFilterSelections,
|
||||
} from "src/engine"
|
||||
import {hints, load, pubkey, canSign, loadAll, deriveEventsMapped} from "src/engine"
|
||||
import {router} from "src/app/util/router"
|
||||
|
||||
export let feed
|
||||
export let filter
|
||||
export let group = null
|
||||
|
||||
const calendarEvents = deriveEventsMapped({
|
||||
filters: [filter],
|
||||
itemToEvent: (item: any) => item.event,
|
||||
eventToItem: (event: TrustedEvent) => {
|
||||
const meta = fromPairs(event.tags)
|
||||
const isOwn = event.pubkey === $pubkey
|
||||
|
||||
return {
|
||||
event,
|
||||
editable: isOwn,
|
||||
id: getAddress(event),
|
||||
title: meta.title || meta.name, // Backwards compat with a bug
|
||||
start: secondsToDate(meta.start),
|
||||
end: secondsToDate(meta.end),
|
||||
backgroundColor: $themeColors[isOwn ? "accent" : "neutral-100"],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const createEvent = () => router.at("notes/create").qp({type: "calendar_event", group}).open()
|
||||
|
||||
const getEventContent = ({event}) => event.title
|
||||
@ -45,54 +55,16 @@
|
||||
|
||||
const onEventClick = ({event: calendarEvent}) => router.at("events").of(calendarEvent.id).open()
|
||||
|
||||
const events = writable(new Map())
|
||||
|
||||
const onEvent = batch(300, (chunk: TrustedEvent[]) => {
|
||||
events.update($events => {
|
||||
for (const e of chunk) {
|
||||
const addr = getAddress(e)
|
||||
const dup = $events.get(addr)
|
||||
|
||||
// Make sure we have the latest version of every event
|
||||
$events.set(addr, dup?.created_at > e.created_at ? dup : e)
|
||||
}
|
||||
|
||||
return $events
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
loadAll(feedFromFilter(filter), {
|
||||
// Load deletes for these events
|
||||
onEvent: batch(300, (chunk: TrustedEvent[]) => {
|
||||
load({
|
||||
relays: hints.merge(chunk.map(e => hints.EventChildren(e))).getUrls(),
|
||||
filters: getReplyFilters(chunk, {kinds: [5]}),
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
let subs = []
|
||||
|
||||
onMount(async () => {
|
||||
const [{filters}] = await feedLoader.compiler.compile(feed)
|
||||
|
||||
subs = getFilterSelections(filters).map(({relay, filters}) =>
|
||||
subscribe({relays: [relay], filters, onEvent}),
|
||||
)
|
||||
})
|
||||
|
||||
onDestroy(() => subs.map(sub => sub.close()))
|
||||
|
||||
$: calendarEvents = Array.from($events.values())
|
||||
.filter(e => !repository.isDeleted(e))
|
||||
.map(e => {
|
||||
const meta = fromPairs(e.tags)
|
||||
const isOwn = e.pubkey === $pubkey
|
||||
|
||||
return {
|
||||
editable: isOwn,
|
||||
id: getAddress(e),
|
||||
title: meta.title || meta.name, // Backwards compat with a bug
|
||||
start: secondsToDate(meta.start),
|
||||
end: secondsToDate(meta.end),
|
||||
backgroundColor: $themeColors[isOwn ? "accent" : "neutral-100"],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -110,7 +82,7 @@
|
||||
plugins={[Interaction, DayGrid]}
|
||||
options={{
|
||||
view: "dayGridMonth",
|
||||
events: calendarEvents,
|
||||
events: $calendarEvents,
|
||||
dateClick: onDateClick,
|
||||
eventClick: onEventClick,
|
||||
eventContent: getEventContent,
|
||||
|
@ -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} />
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {feedFromFilter} from "@welshman/feeds"
|
||||
import {EVENT_TIME} from "@welshman/util"
|
||||
import Calendar from "src/app/shared/Calendar.svelte"
|
||||
|
||||
export let address
|
||||
</script>
|
||||
|
||||
<Calendar group={address} feed={feedFromFilter({kinds: [31923], "#a": [address]})} />
|
||||
<Calendar group={address} filter={{kinds: [EVENT_TIME], "#a": [address]}} />
|
||||
|
@ -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}
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -1,19 +1,27 @@
|
||||
<script>
|
||||
import {ellipsize} from "hurdak"
|
||||
import {derived} from "svelte/store"
|
||||
import {remove} from "@welshman/lib"
|
||||
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, userFollowsByCommunity, 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 members = derived(userFollowsByCommunity, $m => remove($pubkey, $m.get(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)
|
||||
const followMembers = intersection(otherMembers, Array.from($userFollows))
|
||||
|
||||
return followMembers
|
||||
})
|
||||
|
||||
const enter = () => {
|
||||
const route = router.at("groups").of(address).at("notes")
|
||||
@ -31,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}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {isGroupAddress} from "@welshman/util"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import PersonSummary from "src/app/shared/PersonSummary.svelte"
|
||||
@ -24,7 +25,7 @@
|
||||
<Card interactive on:click={() => openPerson(pubkey)}>
|
||||
<PersonSummary inert {pubkey}>
|
||||
<div slot="actions" on:click|stopPropagation>
|
||||
{#if $adminKey && pubkey !== $session.pubkey}
|
||||
{#if $adminKey && pubkey !== $session.pubkey && isGroupAddress(address)}
|
||||
<Anchor on:click={remove} button accent>Remove</Anchor>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,15 +1,26 @@
|
||||
<script lang="ts">
|
||||
import {COMMUNITIES} from "@welshman/util"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
import GroupMember from "src/app/shared/GroupMember.svelte"
|
||||
import {deriveGroup} from 'src/engine'
|
||||
import {load, hints, deriveGroup, communityListsByAddress} from "src/engine"
|
||||
|
||||
export let address
|
||||
|
||||
const group = deriveGroup(address)
|
||||
const filters = [{kinds: [COMMUNITIES], "#a": [address]}]
|
||||
|
||||
$: members =
|
||||
$group.members || $communityListsByAddress.get(address)?.map(l => l.event.pubkey) || []
|
||||
|
||||
load({
|
||||
filters,
|
||||
skipCache: true,
|
||||
relays: hints.merge([hints.WithinContext(address), hints.User()]).getUrls(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<FlexColumn>
|
||||
{#each $group.members || [] as pubkey (pubkey)}
|
||||
{#each members as pubkey (pubkey)}
|
||||
<GroupMember {address} {pubkey} />
|
||||
{:else}
|
||||
<p class="text-center py-12">No members found.</p>
|
||||
|
@ -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>
|
||||
|
@ -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)) {
|
||||
|
@ -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>
|
||||
|
@ -97,7 +97,7 @@
|
||||
<form on:submit|preventDefault={() => onSubmit()}>
|
||||
<AltColor background class="z-feature flex gap-4 overflow-hidden rounded p-3 text-neutral-100">
|
||||
<PersonCircle class="h-10 w-10" pubkey={$pubkey} />
|
||||
<div class="w-full">
|
||||
<div class="w-full min-w-0">
|
||||
<Compose placeholder="What's up?" bind:this={compose} {onSubmit} style="min-height: 3em;" />
|
||||
<div class="p-2">
|
||||
<NoteImages bind:this={images} bind:compose includeInContent />
|
||||
|
@ -1,21 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Scope,
|
||||
feedFromFilter,
|
||||
makeIntersectionFeed,
|
||||
makeKindFeed,
|
||||
makeScopeFeed,
|
||||
} from "@welshman/feeds"
|
||||
import {identity} from "ramda"
|
||||
import Calendar from "src/app/shared/Calendar.svelte"
|
||||
import {env, loadGroupMessages} from "src/engine"
|
||||
import {env, loadGroupMessages, pubkey, userFollows} from "src/engine"
|
||||
|
||||
const feed = $env.FORCE_GROUP
|
||||
? feedFromFilter({kinds: [31923], "#a": [$env.FORCE_GROUP]})
|
||||
: makeIntersectionFeed(makeKindFeed(31923), makeScopeFeed(Scope.Self, Scope.Follows))
|
||||
const filter = $env.FORCE_GROUP
|
||||
? {kinds: [31923], "#a": [$env.FORCE_GROUP]}
|
||||
: {kinds: [31923], authors: [$pubkey, ...$userFollows].filter(identity)}
|
||||
|
||||
if ($env.FORCE_GROUP) {
|
||||
loadGroupMessages([$env.FORCE_GROUP])
|
||||
}
|
||||
</script>
|
||||
|
||||
<Calendar {feed} />
|
||||
<Calendar {filter} />
|
||||
|
@ -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,
|
||||
feeds: [],
|
||||
relays: $env.PLATFORM_RELAYS,
|
||||
members: [$pubkey],
|
||||
list_publicly: false,
|
||||
meta: {
|
||||
kind: null,
|
||||
name: "",
|
||||
about: "",
|
||||
picture: "",
|
||||
image: "",
|
||||
banner: "",
|
||||
},
|
||||
feeds: [],
|
||||
relays: $env.PLATFORM_RELAYS.map(url => ["relay", url]),
|
||||
listing_is_public: false,
|
||||
members: [$pubkey],
|
||||
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}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
@ -71,7 +73,7 @@
|
||||
tabs.push("market")
|
||||
}
|
||||
|
||||
if ($sharedKey) {
|
||||
if ($sharedKey || address.startsWith("34550")) {
|
||||
tabs.push("members")
|
||||
} else if (activeTab === "members") {
|
||||
activeTab = "notes"
|
||||
@ -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} />
|
||||
|
@ -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!")
|
||||
|
@ -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}
|
||||
|
@ -1,7 +1,9 @@
|
||||
<script>
|
||||
import {onMount} from "svelte"
|
||||
import {filter, assoc} from "ramda"
|
||||
import {now} from "@welshman/lib"
|
||||
import {filter, reject, assoc} from "ramda"
|
||||
import {derived} from "svelte/store"
|
||||
import {now, shuffle} from "@welshman/lib"
|
||||
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"
|
||||
@ -11,39 +13,37 @@
|
||||
load,
|
||||
hints,
|
||||
groups,
|
||||
repository,
|
||||
getGroupReqInfo,
|
||||
loadGiftWraps,
|
||||
loadGroupMessages,
|
||||
deriveIsGroupMember,
|
||||
updateCurrentSession,
|
||||
searchGroups,
|
||||
communityListsByAddress,
|
||||
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 {admins} = getGroupReqInfo()
|
||||
const scroller = createScroller(loadMore, {element})
|
||||
const loader = loadGiftWraps()
|
||||
const scroller = createScroller(loadMore, {element})
|
||||
const communityAddrs = Array.from($communityListsByAddress.keys()).filter(
|
||||
a => !groups.key(a).get()?.meta,
|
||||
)
|
||||
|
||||
updateCurrentSession(assoc("groups_last_synced", now()))
|
||||
|
||||
@ -51,15 +51,15 @@
|
||||
|
||||
load({
|
||||
skipCache: true,
|
||||
forcePlatform: false,
|
||||
relays: hints.User().getUrls(),
|
||||
filters: [{kinds: [35834, 34550], authors: admins}],
|
||||
filters: [{kinds: [GROUP, COMMUNITY], limit: 1000 - communityAddrs.length}],
|
||||
})
|
||||
|
||||
load({
|
||||
skipCache: true,
|
||||
forcePlatform: false,
|
||||
relays: hints.User().getUrls(),
|
||||
filters: [{kinds: [35834, 34550], limit: 500}],
|
||||
filters: getIdFilters(shuffle(communityAddrs).slice(0, 1000)),
|
||||
})
|
||||
|
||||
return () => {
|
||||
@ -79,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}
|
||||
@ -88,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>
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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
41
src/domain/group.ts
Normal 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]"
|
@ -1,5 +1,6 @@
|
||||
export * from "./collection"
|
||||
export * from "./feed"
|
||||
export * from "./group"
|
||||
export * from "./handle"
|
||||
export * from "./handler"
|
||||
export * from "./kind"
|
||||
|
@ -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:
|
||||
@ -339,16 +339,16 @@ export const publishToGroupAdmin = async (address, template) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const publishAsGroupAdminPublicly = async (address, template) => {
|
||||
const relays = hints.WithinContext(address).getUrls()
|
||||
export const publishAsGroupAdminPublicly = async (address, template, relays = []) => {
|
||||
const _relays = hints.merge([hints.fromRelays(relays), hints.WithinContext(address)]).getUrls()
|
||||
const adminKey = deriveAdminKeyForGroup(address).get()
|
||||
const event = await sign(template, {sk: adminKey.privkey})
|
||||
|
||||
return publish({event, relays, forcePlatform: false})
|
||||
return publish({event, relays: _relays, forcePlatform: false})
|
||||
}
|
||||
|
||||
export const publishAsGroupAdminPrivately = async (address, template) => {
|
||||
const relays = hints.WithinContext(address).getUrls()
|
||||
export const publishAsGroupAdminPrivately = async (address, template, relays = []) => {
|
||||
const _relays = hints.merge([hints.fromRelays(relays), hints.WithinContext(address)]).getUrls()
|
||||
const adminKey = deriveAdminKeyForGroup(address).get()
|
||||
const sharedKey = deriveSharedKeyForGroup(address).get()
|
||||
|
||||
@ -363,7 +363,7 @@ export const publishAsGroupAdminPrivately = async (address, template) => {
|
||||
const pubs = []
|
||||
|
||||
for (const rumor of rumors) {
|
||||
pubs.push(publish({event: rumor.wrap, relays, forcePlatform: false}))
|
||||
pubs.push(publish({event: rumor.wrap, relays: _relays, forcePlatform: false}))
|
||||
}
|
||||
|
||||
return pubs
|
||||
@ -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)
|
||||
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)
|
||||
: publishAsGroupAdminPrivately(address, template)
|
||||
? publishAsGroupAdminPublicly(address, template, meta.relays)
|
||||
: publishAsGroupAdminPrivately(address, template, meta.relays)
|
||||
}
|
||||
|
||||
export const deleteGroupMeta = address =>
|
||||
|
@ -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 = {
|
||||
|
@ -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.image,
|
||||
picture: meta.image,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
projections.addHandler(27, (e: TrustedEvent) => {
|
||||
const address = Tags.fromEvent(e).groups().values().first()
|
||||
|
||||
|
@ -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"
|
||||
@ -141,17 +142,17 @@ export const getStaleAddrs = (addrs: string[]) => {
|
||||
for (const addr of addrs) {
|
||||
const attempts = attemptedAddrs.get(addr) | 0
|
||||
|
||||
if (attempts > 1) {
|
||||
if (attempts > 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
attemptedAddrs.set(addr, attempts + 1)
|
||||
const meta = get(deriveGroupMeta(addr))
|
||||
|
||||
const group = groups.key(addr).get()
|
||||
|
||||
if (!group?.meta) {
|
||||
if (!meta) {
|
||||
stale.add(addr)
|
||||
}
|
||||
|
||||
attemptedAddrs.set(addr, attempts + 1)
|
||||
}
|
||||
|
||||
return Array.from(stale)
|
||||
|
@ -6,7 +6,9 @@ import {
|
||||
PROFILE,
|
||||
HANDLER_INFORMATION,
|
||||
NAMED_BOOKMARKS,
|
||||
COMMUNITIES,
|
||||
FEED,
|
||||
MUTES,
|
||||
FOLLOWS,
|
||||
APP_DATA,
|
||||
} from "@welshman/util"
|
||||
@ -50,10 +52,10 @@ const getFiltersForKey = (key: string, authors: string[]) => {
|
||||
case "pubkey/relays":
|
||||
return [{authors, kinds: [RELAYS]}]
|
||||
case "pubkey/profile":
|
||||
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION]}]
|
||||
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}]
|
||||
case "pubkey/user":
|
||||
return [
|
||||
{authors, kinds: [PROFILE, RELAYS, FOLLOWS, APP_DATA]},
|
||||
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, APP_DATA]},
|
||||
{authors, kinds: [APP_DATA], "#d": Object.values(appDataKeys)},
|
||||
]
|
||||
}
|
||||
|
@ -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,
|
||||
@ -35,6 +35,7 @@ import {
|
||||
uniq,
|
||||
uniqBy,
|
||||
now,
|
||||
intersection,
|
||||
sort,
|
||||
groupBy,
|
||||
indexBy,
|
||||
@ -42,6 +43,8 @@ import {
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
WRAP,
|
||||
COMMUNITY,
|
||||
GROUP,
|
||||
WRAP_NIP04,
|
||||
COMMUNITIES,
|
||||
READ_RECEIPT,
|
||||
@ -102,6 +105,7 @@ import type {
|
||||
PublishedListFeed,
|
||||
PublishedSingleton,
|
||||
PublishedList,
|
||||
PublishedGroupMeta,
|
||||
RelayPolicy,
|
||||
Handle,
|
||||
} from "src/domain"
|
||||
@ -129,6 +133,7 @@ import {
|
||||
makeRelayPolicy,
|
||||
filterRelaysByNip,
|
||||
displayRelayUrl,
|
||||
readGroupMeta,
|
||||
} from "src/domain"
|
||||
import type {
|
||||
Channel,
|
||||
@ -177,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")
|
||||
@ -693,6 +699,18 @@ export const communityListsByPubkey = withGetter(
|
||||
derived(communityLists, $ls => indexBy($l => $l.event.pubkey, $ls)),
|
||||
)
|
||||
|
||||
export const communityListsByAddress = derived(communityLists, $communityLists => {
|
||||
const m = new Map<string, PublishedSingleton[]>()
|
||||
|
||||
for (const list of $communityLists) {
|
||||
for (const a of getSingletonValues("a", list)) {
|
||||
pushToMapKey(m, a, list)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
})
|
||||
|
||||
export const getCommunityList = (pk: string) =>
|
||||
communityListsByPubkey.get().get(pk) as PublishedSingleton | undefined
|
||||
|
||||
@ -704,54 +722,55 @@ export const getCommunities = (pk: string) => getSingletonValues("a", getCommuni
|
||||
export const deriveCommunities = (pk: string) =>
|
||||
derived(communityListsByPubkey, m => getSingletonValues("a", m.get(pk)))
|
||||
|
||||
export const userFollowsByCommunity = derived(communityLists, $communityLists => {
|
||||
const m = new Map<string, string[]>()
|
||||
|
||||
for (const list of $communityLists) {
|
||||
for (const a of getSingletonValues("a", list)) {
|
||||
pushToMapKey(m, a, list.event.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
})
|
||||
|
||||
// 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), userFollowsByCommunity],
|
||||
([$groups, $userFollowsByCommunity]) => {
|
||||
const options = $groups
|
||||
.filter(group => !repository.deletes.has(group.address))
|
||||
.map(group => ({group, score: $userFollowsByCommunity.get(group.address)?.length || 0}))
|
||||
export const deriveGroupMeta = (address: string) =>
|
||||
derived(groupMetaByAddress, $m => $m.get(address))
|
||||
|
||||
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()
|
||||
@ -1140,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[]) =>
|
||||
@ -1620,7 +1640,7 @@ export const publish = ({forcePlatform = true, ...request}: MyPublishRequest) =>
|
||||
return pub
|
||||
}
|
||||
|
||||
export const sign = (template, opts: {anonymous?: boolean; sk?: string}) => {
|
||||
export const sign = (template, opts: {anonymous?: boolean; sk?: string} = {}) => {
|
||||
if (opts.anonymous) {
|
||||
return signer.get().signWithKey(template, generatePrivateKey())
|
||||
}
|
||||
|
@ -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)
|
@ -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))
|
||||
|
||||
|
@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<div
|
||||
style={$$props.style || "min-height: 6rem"}
|
||||
class={cx($$props.class, "w-full min-w-0 outline-0")}
|
||||
class={cx($$props.class, "w-full min-w-0 whitespace-pre-line outline-0")}
|
||||
{autofocus}
|
||||
contenteditable
|
||||
bind:this={input}
|
||||
|
Loading…
Reference in New Issue
Block a user