Remove nip28 chat

This commit is contained in:
Jonathan Staab 2023-09-27 09:59:22 -07:00
parent c4541e2fa6
commit 95a24abd0f
21 changed files with 38 additions and 620 deletions

View File

@ -118,4 +118,5 @@ An important goal for the UX is speed without reflow due to late-arriving events
I'm still working through the privacy tradeoff. In general, I opt for the public version of any primitive (follows, relays list) so that more sophisticated functionality can be built on top of open social graphs. However, in the case of application-specific settings and usage data, there is no benefit to sharing that information publicly, and so it is encrypted. One of the main goals of Coracle is also to eventually support private groups alongside public notes, which will present a tricky design challenge.
Coracle only has support for DMs and chat because it was added early on and people use it. These are not the priority of the project, and hopefully can be replaced with an embedded micro-app at some point in the future. Insofar as certain NIPs can augment regular social content (public and private) however, they should be supported. This includes things like Data Vending Machines and Client recommendations. Rendering support is included for long-form posts, profile updates, and more, and advanced search and custom feeds are an area of research I'm interested in pushing forward.
Coracle only has support for DMs because it was added early on and people use it. These are not the priority of the project, and hopefully can be replaced with an embedded micro-app at some point in the future. Insofar as certain NIPs can augment regular social content (public and private) however, they should be supported. This includes things like Data Vending Machines and Client recommendations. Rendering support is included for long-form posts, profile updates, and more, and advanced search and custom feeds are an area of research I'm interested in pushing forward.

View File

@ -10,28 +10,36 @@ If you like Coracle and want to support its development, you can donate sats via
- [x] Threads/social
- [x] Profile search using NIP-50
- [x] Login via extension
- [x] Login via extension, nsecbunker, and pubkey
- [x] Profile sharing via QR codes
- [x] NIP 05 verification
- [x] Bech32 entity search
- [x] Notifications
- [x] Chat and direct messages
- [x] NIP 65 relay selection and NIP 32 relay reviews
- [x] NIP 89 app recommendations
- [x] NIP 32 labeling and recommendations
- [x] Bech32 entity search and scan
- [x] Mention, reply, and reaction notifications
- [x] Direct messages - NIP 04 and NIP 24
- [x] Note composition with mentions and topics
- [x] Profile pages, follow/unfollow
- [x] Thread and person muting, collapse thread
- [x] Smart relay selection and display
- [x] Content warnings, mute, and keyword mute
- [x] Profile pages, follow/unfollow, follow/follower count
- [x] Thread muting, collapse thread
- [x] Invoice, quote, mention, link, image, and video rendering
- [x] Installable as a progressive web app
- [x] Integrated media uploads
- [x] Lightning zaps
- [x] Integrated media uploads via nostr.build
- [x] Lightning zaps and reactions
- [x] Feeds customizable by person, relay, and topic using NIP-51
- [x] AUTH (NIP-42) support for paid relays
- [x] Multiplextr support for reducing bandwidth
- [x] Profile and note metadata
- [x] White-labeling support
- [ ] Exportable copy of all user events
- [x] NIP 51 person lists
- [x] Exportable copy of all user events
- [ ] Reporting and basic distributed moderation
- [ ] Content and person recommendations
- [ ] Private groups including administration, moderation, and membership
- [ ] Cross-posting between groups and public nostr
- [ ] Public and private calendar events
- [ ] Public and private marketplaces
You can find a more complete changelog [here](./CHANGELOG.md).

View File

@ -10,9 +10,7 @@
let scrollY = 0
let playerIsOpen = false
$: showButtons = !$location.pathname.match(
/conversations|channels|chat|relays|keys|settings|logout$/
)
$: showButtons = !$location.pathname.match(/conversations|channels|relays|keys|settings|logout$/)
const scrollToTop = () => document.body.scrollIntoView({behavior: "smooth"})

View File

@ -2,7 +2,6 @@
import {nip19} from "nostr-tools"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
import ChatEdit from "src/app/views/ChatEdit.svelte"
import ChannelCreate from "src/app/views/ChannelCreate.svelte"
import Login from "src/app/views/Login.svelte"
import LoginConnect from "src/app/views/LoginConnect.svelte"
@ -61,8 +60,6 @@
<Onboarding stage={m.stage} />
{:else if m.type === "channel/create"}
<ChannelCreate />
{:else if m.type === "chat/edit"}
<ChatEdit {...m} />
{:else if m.type === "login/intro"}
<Login />
{:else if m.type === "login/privkey"}

View File

@ -4,8 +4,6 @@
import {base64DecodeOrPlainWebSocketURL} from "src/util/misc"
import Notifications from "src/app/views/Notifications.svelte"
import Bech32Entity from "src/app/views/Bech32Entity.svelte"
import ChatDetail from "src/app/views/ChatDetail.svelte"
import ChatList from "src/app/views/ChatList.svelte"
import Feeds from "src/app/views/Feeds.svelte"
import Explore from "src/app/views/Explore.svelte"
import UserKeys from "src/app/views/UserKeys.svelte"
@ -47,12 +45,6 @@
<PersonDetail npub={params.npub} />
{/key}
</TypedRoute>
<TypedRoute path="/chat" component={ChatList} />
<TypedRoute path="/chat/:entity" let:params>
{#key params.entity}
<ChatDetail entity={params.entity} />
{/key}
</TypedRoute>
<TypedRoute path="/conversations">
<MessagesList activeTab="conversations" />
</TypedRoute>

View File

@ -3,7 +3,6 @@
import {modal, theme, installPrompt} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
import {
hasNewNip28Messages,
hasNewNip04Messages,
hasNewNip24Messages,
hasNewNotifications,
@ -87,15 +86,11 @@
</a>
</li>
{/if}
<li class="relative">
<li class="cursor-pointer">
<a
class="block px-4 py-2 transition-all hover:bg-accent hover:text-white"
on:click={openNip28Chat}>
<i class="fa fa-comment mr-2" /> Chat
{#if $hasNewNip28Messages}
<div
class="absolute left-7 top-2 h-2 w-2 rounded border border-solid border-white bg-accent" />
{/if}
</a>
</li>
<li class="cursor-pointer">

View File

@ -25,7 +25,6 @@
peopleWithName,
session,
canUseGiftWrap,
hasNewNip28Messages,
hasNewNip04Messages,
hasNewNip24Messages,
hasNewNotifications,
@ -183,7 +182,7 @@
<div class="app-logo flex cursor-pointer items-center gap-2" on:click={toggleMenu}>
<img alt="App Logo" src={logoUrl} class="w-10" />
<h1 class="staatliches pt-1 text-3xl">{appName}</h1>
{#if $hasNewNotifications || $hasNewNip28Messages || hasNewDMs}
{#if $hasNewNotifications || hasNewDMs}
<div
class="absolute left-8 top-4 h-2 w-2 rounded border border-solid border-white bg-accent" />
{/if}

View File

@ -1,33 +1,29 @@
<script lang="ts">
import {defaultTo} from "ramda"
import {navigate} from "svelte-routing"
import {nip19} from "nostr-tools"
import {tryJson} from "src/util/misc"
import {Tags} from "src/util/nostr"
import Card from "src/partials/Card.svelte"
import Content from "src/partials/Content.svelte"
import ImageCircle from "src/partials/ImageCircle.svelte"
import {channels} from "src/engine"
export let note
const {name, picture, about} = tryJson(() => JSON.parse(note.content))
const channel = channels
.key(note.id)
.derived(defaultTo({id: note.id, meta: {name, picture, about}}))
const noteId = nip19.noteEncode(note.kind === 40 ? note.id : Tags.from(note).getMeta("e"))
const goToChat = () => window.open(`https://chat.coracle.social/chat/${noteId}`)
</script>
<Card interactive invertColors on:click={() => navigate(`/chat/${noteId}`)}>
<Card interactive invertColors on:click={goToChat}>
<Content>
<div class="flex items-center gap-2">
{#if $channel.meta?.picture}
<ImageCircle size={10} src={$channel.meta?.picture} />
{#if picture}
<ImageCircle size={10} src={picture} />
{/if}
<h3 class="staatliches text-2xl">{$channel.meta?.name}</h3>
<h3 class="staatliches text-2xl">{name}</h3>
</div>
{#if $channel.meta?.about}
<p>{$channel.meta?.about}</p>
{#if about}
<p>{about}</p>
{/if}
</Content>
</Card>

View File

@ -1,107 +0,0 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte"
import {assoc} from "ramda"
import {formatTimestamp} from "src/util/misc"
import {toHex} from "src/util/nostr"
import {modal} from "src/partials/state"
import Channel from "src/partials/Channel.svelte"
import Anchor from "src/partials/Anchor.svelte"
import ImageCircle from "src/partials/ImageCircle.svelte"
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
import NoteContent from "src/app/shared/NoteContent.svelte"
import {
canSign,
session,
channels,
imgproxy,
publishNip28Message,
joinNip28Channel,
leaveNip28Channel,
listenForNip28Messages,
publishNip28ChannelChecked,
} from "src/engine"
export let entity
const id = toHex(entity)
const channel = channels.key(id)
publishNip28ChannelChecked(id)
const join = () => joinNip28Channel(id)
const leave = () => leaveNip28Channel(id)
const edit = () => modal.push({type: "chat/edit", channel: $channel})
const sendMessage = async content => {
const pub = await publishNip28Message(id, content)
return pub.result
}
onMount(() => {
const sub = listenForNip28Messages(id)
return () => sub.close()
})
onDestroy(() => {
publishNip28ChannelChecked(id)
// Save on memory by deleting messages we don't care about
if (!$channel?.nip28?.joined) {
channel.update(assoc("messages", []))
}
})
$: picture = imgproxy($channel?.meta?.picture, {w: 96, h: 96})
document.title = $channel?.meta?.name || "Coracle Chat"
</script>
<Channel messages={$channel?.messages || []} {sendMessage}>
<div slot="header" class="flex h-16 items-start gap-4 overflow-hidden p-2">
<div class="flex items-center gap-4 pt-1">
<Anchor type="unstyled" class="fa fa-arrow-left cursor-pointer text-2xl" href="/chat" />
<ImageCircle size={10} src={picture} />
</div>
<div class="flex h-12 flex-grow flex-col overflow-hidden pt-px">
<div class="font-bold">{$channel?.meta?.name || ""}</div>
<div>{$channel?.meta?.about || ""}</div>
</div>
<div class="flex h-12 flex-col pt-px">
<div class="flex w-full items-center justify-between">
<div class="flex gap-2">
{#if $channel?.nip28?.owner === $session?.pubkey}
<button class="cursor-pointer text-sm" on:click={edit}>
<i class="fa-solid fa-edit" /> Edit
</button>
{/if}
{#if $channel?.nip28?.joined}
<Anchor theme="button" killEvent class="flex items-center gap-2" on:click={leave}>
<i class="fa fa-right-from-bracket" />
<span>Leave</span>
</Anchor>
{:else if $canSign}
<Anchor theme="button" killEvent class="flex items-center gap-2" on:click={join}>
<i class="fa fa-right-to-bracket" />
<span>Join</span>
</Anchor>
{/if}
</div>
</div>
</div>
</div>
<div slot="message" let:message>
{#if message.showProfile}
<div class="flex items-center justify-between gap-4">
<PersonBadgeSmall pubkey={message.pubkey} />
<p class="text-sm text-gray-1">{formatTimestamp(message.created_at)}</p>
</div>
{/if}
<div class="my-1 ml-6 overflow-hidden text-ellipsis">
<NoteContent showEntire note={message} />
</div>
</div>
</Channel>

View File

@ -1,64 +0,0 @@
<script lang="ts">
import Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte"
import Textarea from "src/partials/Textarea.svelte"
import ImageInput from "src/partials/ImageInput.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {toast, modal} from "src/partials/state"
import {joinNip28Channel, publishNip28ChannelCreate, publishNip28ChannelUpdate} from "src/engine"
import {toastProgress} from "src/app/state"
export let channel = {name: null, id: null, about: null, picture: null}
const submit = async e => {
e.preventDefault()
if (!channel.name) {
toast.show("error", "Please enter a name for your room.")
} else {
const {id, ...content} = channel
if (id) {
const pub = await publishNip28ChannelUpdate(id, content)
pub.on("progress", toastProgress)
} else {
const pub = await publishNip28ChannelCreate(content)
joinNip28Channel(pub.event.id)
}
modal.pop()
}
}
</script>
<form on:submit={submit} class="flex justify-center py-12">
<Content>
<div class="mb-4 flex flex-col items-center justify-center">
<h1 class="staatliches text-6xl">Name your room</h1>
</div>
<div class="flex w-full flex-col gap-8">
<div class="flex flex-col gap-1">
<strong>Room name</strong>
<Input type="text" name="name" wrapperClass="flex-grow" bind:value={channel.name}>
<i slot="before" class="fa-solid fa-tag" />
</Input>
<p class="text-sm text-gray-1">The room's name can be changed anytime.</p>
</div>
<div class="flex flex-col gap-1">
<strong>Room information</strong>
<Textarea name="about" bind:value={channel.about} />
<p class="text-sm text-gray-1">
Give people an idea of what kind of conversations will be happening here.
</p>
</div>
<div class="flex flex-col gap-1">
<strong>Picture</strong>
<ImageInput icon="image" bind:value={channel.picture} />
<p class="text-sm text-gray-1">A picture to help people remember your room.</p>
</div>
<Anchor tag="button" theme="button" type="submit" class="text-center">Done</Anchor>
</div>
</Content>
</form>

View File

@ -1,130 +0,0 @@
<script lang="ts">
import {onMount} from "svelte"
import {batch, seconds} from "hurdak"
import {pluck, filter, uniq, path} from "ramda"
import {now, createScroller} from "src/util/misc"
import {Tags} from "src/util/nostr"
import {modal} from "src/partials/state"
import Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte"
import ChatListItem from "src/app/views/ChatListItem.svelte"
import type {Event, Filter} from "src/engine"
import {
session,
canSign,
follows,
stateKey,
searchNip28Channels,
load,
getPubkeyHints,
searchableRelays,
sortChannels,
nip28ChannelsWithMeta,
loadPubkeys,
getPubkeysWithDefaults,
} from "src/engine"
let q = ""
let results = []
let limit = 5
const loadMore = async () => {
limit += 5
}
const scroller = createScroller(loadMore)
const channels = nip28ChannelsWithMeta.derived(sortChannels)
const joined = channels.derived(filter(path(["nip28", "joined"])))
const searchChannels = q => {
if (q.length > 3) {
load({
relays: $searchableRelays,
filters: [{kinds: [40, 41], search: q}],
})
}
}
$: searchChannels(q)
$: results = $searchNip28Channels(q).slice(0, limit)
document.title = "Chat"
onMount(() => {
const relays = getPubkeyHints.limit(3).getHints($stateKey, "read")
const authors = getPubkeysWithDefaults($follows)
const since = now() - seconds(1, "day")
const filters = [
{kinds: [40, 41], authors, limit: 100},
{kinds: [42], since, authors, limit: 100},
] as Filter[]
if ($session) {
filters.push({kinds: [40, 41], authors: [$session.pubkey]})
}
// Pull some relevant channels by grabbing recent messages
load({
relays,
filters,
onEvent: batch(500, (events: Event[]) => {
const channelIds = uniq(
events.filter(e => e.kind === 42).map(e => Tags.from(e).getMeta("e"))
)
loadPubkeys(pluck("pubkey", events))
if (channelIds.length > 0) {
load({
relays,
filters: [
{kinds: [40], ids: channelIds},
{kinds: [41], "#e": channelIds},
],
})
}
}),
})
return () => {
scroller.stop()
}
})
</script>
<Content>
{#if $canSign}
<div class="flex justify-between">
<div class="flex items-center gap-2">
<i class="fa fa-server fa-lg" />
<h2 class="staatliches text-2xl">Your rooms</h2>
</div>
<Anchor theme="button-accent" on:click={() => modal.push({type: "chat/edit"})}>
<i class="fa-solid fa-plus" /> Create Room
</Anchor>
</div>
{#each $joined as channel (channel.id)}
<ChatListItem {channel} />
{:else}
<p class="text-center py-8">You haven't yet joined any rooms.</p>
{/each}
<div class="mb-2 border-b border-solid border-gray-6 pt-2" />
<div class="flex items-center gap-2">
<i class="fa fa-earth-asia fa-lg" />
<h2 class="staatliches text-2xl">Other rooms</h2>
</div>
{/if}
<div class="flex-grow">
<Input bind:value={q} type="text" placeholder="Search rooms">
<i slot="before" class="fa-solid fa-search" />
</Input>
</div>
{#each results as channel (channel.id)}
<ChatListItem {channel} />
{:else}
<small class="text-center"> No matching rooms found </small>
{/each}
</Content>

View File

@ -1,57 +0,0 @@
<script lang="ts">
import {nip19} from "nostr-tools"
import {navigate} from "svelte-routing"
import {fly} from "src/util/transition"
import {ellipsize} from "hurdak"
import Anchor from "src/partials/Anchor.svelte"
import {canSign, hasNewMessages, imgproxy, joinNip28Channel, leaveNip28Channel} from "src/engine"
export let channel
const enter = () => navigate(`/chat/${nip19.noteEncode(channel.id)}`)
const join = () => joinNip28Channel(channel.id)
const leave = () => leaveNip28Channel(channel.id)
// Accommodate data urls from legacy
const picture =
channel.meta?.picture?.length > 500
? channel.meta.picture
: imgproxy(channel.meta.picture, {w: 112, h: 112})
$: showBadge = channel.nip28.joined && hasNewMessages(channel)
</script>
<button
class="relative flex cursor-pointer gap-4 rounded border border-solid border-gray-6 bg-gray-7 px-4 py-6 transition-all hover:bg-gray-6"
on:click={enter}
in:fly={{y: 20}}>
<div
class="h-14 w-14 shrink-0 overflow-hidden rounded-full border border-solid border-white bg-cover bg-center"
style={`background-image: url(${picture})`} />
{#if showBadge}
<div class="absolute left-2 top-2 h-2 w-2 rounded bg-accent" />
{/if}
<div class="flex min-w-0 flex-grow flex-col justify-start gap-2">
<div class="flex flex-grow items-start justify-between gap-2">
<h2 class="text-lg">
{channel.meta?.name || ""}
</h2>
{#if channel.nip28.joined}
<Anchor theme="button" killEvent class="flex items-center gap-2" on:click={leave}>
<i class="fa fa-right-from-bracket" />
<span>Leave</span>
</Anchor>
{:else if $canSign}
<Anchor theme="button" killEvent class="flex items-center gap-2" on:click={join}>
<i class="fa fa-right-to-bracket" />
<span>Join</span>
</Anchor>
{/if}
</div>
{#if channel.meta?.about}
<p class="text-start text-gray-1">
{ellipsize(channel.meta.about, 300)}
</p>
{/if}
</div>
</button>

View File

@ -1,11 +1,11 @@
import {generatePrivateKey} from "nostr-tools"
import {assoc, assocPath, path, prop, fromPairs, pluck, whereEq, when, map} from "ramda"
import {assoc, whereEq, when, map} from "ramda"
import {createMapOf} from "hurdak"
import {now} from "src/util/misc"
import {appDataKeys} from "src/util/nostr"
import {EventKind} from "src/engine/events/model"
import {Publisher, publishEvent, mention} from "src/engine/network/utils"
import {selectHints, getInboxHints, getPubkeyHints} from "src/engine/relays/utils"
import {getInboxHints, getPubkeyHints} from "src/engine/relays/utils"
import {user, nip04, nip59} from "src/engine/session/derived"
import {setAppData} from "src/engine/session/commands"
import {channels} from "./state"
@ -79,45 +79,3 @@ export const nip24MarkChannelRead = (pubkey: string) => {
publishNip24Read()
}
export const publishNip28ChannelCreate = content =>
publishEvent(40, {content: JSON.stringify(content)})
export const publishNip28ChannelUpdate = (id, content) =>
publishEvent(41, {content: JSON.stringify(content), tags: [["e", id]]})
export const publishNip28Message = (id, content) => {
const channel = channels.key(id).get()
const [hint] = selectHints(channel?.relays || [])
return publishEvent(42, {content, tags: [["e", id, hint, "root"]]})
}
export const publishNip28ChannelChecked = (id: string) => {
const lastChecked = fromPairs(
channels
.get()
.filter(prop("last_checked"))
.map(r => [r.id, r.last_checked])
)
return setAppData(appDataKeys.NIP28_LAST_CHECKED, {...lastChecked, [id]: now()})
}
export const saveNip28Channels = () =>
setAppData(
appDataKeys.NIP28_ROOMS_JOINED,
pluck("id", channels.get().filter(path(["nip28", "joined"])))
)
export const joinNip28Channel = (id: string) => {
channels.key(id).update(assocPath(["nip28", "joined"], true))
return saveNip28Channels()
}
export const leaveNip28Channel = (id: string) => {
channels.key(id).update(assocPath(["nip28", "joined"], false))
return saveNip28Channels()
}

View File

@ -1,6 +1,5 @@
import {find, filter, path, whereEq} from "ramda"
import {hasNewMessages, getChannelSearch} from "./utils"
import type {Channel} from "./model"
import {find, filter, whereEq} from "ramda"
import {hasNewMessages} from "./utils"
import {channels} from "./state"
// Nip04
@ -14,15 +13,3 @@ export const hasNewNip04Messages = nip04Channels.derived(find(hasNewMessages))
export const nip24Channels = channels.throttle(300).derived(filter(whereEq({type: "nip24"})))
export const hasNewNip24Messages = nip24Channels.derived(find(hasNewMessages))
// Nip28
export const nip28ChannelsWithMeta = channels
.throttle(300)
.derived(filter((c: Channel) => c.meta && c.type === "nip28"))
export const nip28ChannelsForUser = nip28ChannelsWithMeta.derived(filter(path(["nip28", "joined"])))
export const searchNip28Channels = nip28ChannelsWithMeta.derived(getChannelSearch)
export const hasNewNip28Messages = nip28ChannelsForUser.derived(find(hasNewMessages))

View File

@ -2,23 +2,13 @@ import type {Event} from "src/engine/events/model"
export type Channel = {
id: string
type: "nip28" | "nip04" | "nip44"
type: "nip04" | "nip44"
relays: string[]
messages: Event[]
last_sent?: number
last_received?: number
last_checked?: number
meta_updated_at?: number
meta?: {
name?: string
about?: string
picture?: string
}
nip04?: {
plaintext: string
}
nip28?: {
owner?: string
joined?: string
}
}

View File

@ -1,14 +1,12 @@
import {prop, map, assocPath, pluck, last, uniqBy, uniq} from "ramda"
import {prop, uniqBy, uniq} from "ramda"
import {tryFunc, sleep} from "hurdak"
import {tryJson} from "src/util/misc"
import {Tags, appDataKeys} from "src/util/nostr"
import type {Event} from "src/engine/events/model"
import {EventKind} from "src/engine/events/model"
import {sessions} from "src/engine/session/state"
import {Signer, Nip04, getNdk} from "src/engine/session/utils"
import {getEventHints} from "src/engine/relays/utils"
import {projections} from "src/engine/core/projections"
import {updateRecord, updateStore} from "src/engine/core/commands"
import type {Channel} from "./model"
import {channels} from "./state"
import {getNip24ChannelId} from "./utils"
@ -85,58 +83,6 @@ projections.addHandler(EventKind.AppData, async e => {
})
})
}
if (
d === appDataKeys.NIP28_ROOMS_JOINED &&
e.created_at > (session.nip28_channels_last_joined || 0)
) {
sessions.update(assocPath([session.pubkey, "nip28_channels_last_joined"], e.created_at))
await tryJson(async () => {
const channelIds = JSON.parse(await nip04.decryptAsUser(e.content, e.pubkey))
// Just a bug from when I was building the feature, remove someday
if (!Array.isArray(channelIds)) {
return
}
channels.update(
map(channel => {
if (channel.nip28?.joined && !channelIds.includes(channel.id)) {
return assocPath(["nip28", "joined"], false, channel)
} else if (!channel.nip28?.joined && channelIds.includes(channel.id)) {
return assocPath(["nip28", "joined"], true, channel)
}
return channel
})
)
})
}
if (d === appDataKeys.NIP28_LAST_CHECKED) {
await tryJson(async () => {
const payload = JSON.parse(await nip04.decryptAsUser(e.content, e.pubkey))
channels.mapStore.update($channels => {
for (const key of Object.keys(payload)) {
// Backwards compat from when we used to prefix id/pubkey
const id = last(key.split("/"))
const channel = $channels.get(id)
const last_checked = Math.max(payload[id], channel?.last_checked || 0)
// A bunch of junk got added to this setting. Integer keys, settings, etc
if (isNaN(last_checked) || last_checked < 1577836800) {
continue
}
$channels.set(id, {...channel, id, last_checked})
}
return $channels
})
})
}
})
projections.addHandler(EventKind.Nip04Message, async e => {
@ -210,65 +156,3 @@ projections.addHandler(EventKind.Nip44Message, e => {
})
}
})
projections.addHandler(EventKind.ChannelCreation, (e: Event) => {
const meta = tryJson(() => JSON.parse(e.content))
const relays = Tags.from(e).relays()
if (meta?.name) {
channels.key(e.id).update($channel => ({
...updateRecord($channel, e.created_at, {meta, relays}),
nip28: {...$channel?.nip28, owner: e.pubkey},
type: "nip28",
}))
}
})
projections.addHandler(EventKind.ChannelMetadata, (e: Event) => {
const channelId = Tags.from(e).getMeta("e")
if (!channelId) {
return
}
const channel = channels.key(channelId).get()
if (e.pubkey !== channel?.nip28?.owner) {
return
}
const meta = tryJson(() => JSON.parse(e.content))
const relays = Tags.from(e).relays()
if (meta?.name) {
updateStore(channels.key(channelId), e.created_at, {meta, relays})
}
})
projections.addHandler(EventKind.ChannelMessage, (e: Event) => {
const tags = Tags.from(e)
const channelId = tags.getMeta("e")
const pubkeys = pluck("pubkey", Object.values(sessions.get()))
if (!channelId) {
return
}
channels.key(channelId).update($channel => {
const updates = {
...$channel,
id: channelId,
type: "nip28",
relays: uniq([...tags.relays(), ...($channel?.relays || [])]),
messages: uniqBy(prop("id"), [e, ...($channel?.messages || [])]),
}
if (pubkeys.includes(e.pubkey)) {
updates.last_sent = Math.max(updates.last_sent || 0, e.created_at)
} else {
updates.last_received = Math.max(updates.last_received || 0, e.created_at)
}
return updates as Channel
})
})

View File

@ -3,8 +3,7 @@ import {batch, seconds} from "hurdak"
import {EventKind} from "src/engine/events/model"
import {pubkey} from "src/engine/session/state"
import {load, loadPubkeys, subscribe} from "src/engine/network/utils"
import {getInboxHints, selectHints, getPubkeyHints, getUserRelayUrls} from "src/engine/relays/utils"
import {channels} from "./state"
import {getInboxHints, getPubkeyHints, getUserRelayUrls} from "src/engine/relays/utils"
import {getNip24ChannelPubkeys} from "./utils"
import {nip24Channels, nip04Channels} from "./derived"
@ -56,19 +55,6 @@ export const loadAllNip24Messages = () => {
})
}
export const listenForNip28Messages = channelId => {
const channel = channels.key(channelId).get()
const relays = selectHints(channel?.relays || [])
return subscribe({
relays,
filters: [
{kinds: [40], ids: [channelId]},
{kinds: [41, 42], "#e": [channelId]},
],
})
}
export const listenForNip59Messages = () => {
const $pubkey = pubkey.get()

View File

@ -1,5 +1,4 @@
import {sortBy, uniq, identity} from "ramda"
import {fuzzy} from "src/util/misc"
import type {Channel} from "./model"
export const sortChannels = sortBy(
@ -9,9 +8,6 @@ export const sortChannels = sortBy(
export const hasNewMessages = (c: Channel) =>
c.last_received > Math.max(c.last_sent || 0, c.last_checked || 0)
export const getChannelSearch = $channels =>
fuzzy($channels, {keys: ["meta.name", "meta.about"], threshold: 0.3})
export const getNip24ChannelId = (pubkeys: string[]) => sortBy(identity, uniq(pubkeys)).join(",")
export const getNip24ChannelPubkeys = (id: string) => id.split(",")

View File

@ -10,7 +10,6 @@ import {_events} from "src/engine/events/state"
import {events, isEventMuted} from "src/engine/events/derived"
import {mergeHints, getPubkeyHints} from "src/engine/relays/utils"
import {subscribe, subscribePersistent} from "src/engine/network/utils"
import {nip28ChannelsForUser} from "src/engine/channels/derived"
const onNotificationEvent = batch(300, (chunk: Event[]) => {
const kinds = getNotificationKinds()
@ -62,8 +61,6 @@ export const loadNotifications = () => {
export const listenForNotifications = async () => {
const pubkeys = Object.keys(sessions.get())
const channelIds = pluck("id", nip28ChannelsForUser.get())
const relays = mergeHints(pubkeys.map(pk => getPubkeyHints(pk, "read")))
const eventIds: string[] = doPipe(events.get(), [
@ -82,11 +79,6 @@ export const listenForNotifications = async () => {
{kinds: noteKinds, "#p": pubkeys, limit: 1},
]
// Chat
if (channelIds.length > 0) {
filters.push({kinds: [42], "#e": channelIds, limit: 1})
}
// Replies
if (eventIds.length > 0) {
filters.push({kinds: noteKinds, "#e": eventIds, limit: 1})

View File

@ -7,5 +7,4 @@ export type Session = {
settings?: Record<string, any>
settings_updated_at?: number
notifications_last_checked?: number
nip28_channels_last_joined?: number
}

View File

@ -28,8 +28,6 @@ export const appDataKeys = {
USER_SETTINGS: "nostr-engine/User/settings/v1",
NIP04_LAST_CHECKED: "nostr-engine/Nip04/last_checked/v1",
NIP24_LAST_CHECKED: "nostr-engine/Nip24/last_checked/v1",
NIP28_LAST_CHECKED: "nostr-engine/Nip28/last_checked/v1",
NIP28_ROOMS_JOINED: "nostr-engine/Nip28/rooms_joined/v1",
}
export class Tags {