Add tags to notes by parsing content, add note delete button, add mentions to quotes, increase routes table and change sort order to hopefully get better relay hints, upgrade mentions from square bracket notation to bech32 embeds

This commit is contained in:
Jonathan Staab 2023-06-07 05:52:46 -07:00
parent b8a4232683
commit 4ccf12efa8
11 changed files with 95 additions and 62 deletions

View File

@ -3,6 +3,15 @@
# 0.2.30 # 0.2.30
- [x] Prefer followed users when mentioning people - [x] Prefer followed users when mentioning people
- [x] Open people in a modal when it makes sense
- [x] Fix regex for urls
- [x] Fix note sharing bug
- [x] Add mention mark to e tags embedded in notes
- [x] Add tags to notes by parsing content
- [x] Add note delete button
- [x] Add mentions to quotes
- [x] Increase routes table size and change sort order to hopefully get better relay hints
- [x] Upgrade mentions from square bracket notation to bech32 embeds
# 0.2.29 # 0.2.29

View File

@ -1,4 +1,4 @@
import {map, pick, uniqBy} from "ramda" import {pick, uniqBy} from "ramda"
import {get} from "svelte/store" import {get} from "svelte/store"
import {doPipe} from "hurdak/lib/hurdak" import {doPipe} from "hurdak/lib/hurdak"
import {parseContent, Tags, roomAttrs, displayPerson, findRoot, findReply} from "src/util/nostr" import {parseContent, Tags, roomAttrs, displayPerson, findRoot, findReply} from "src/util/nostr"
@ -51,39 +51,21 @@ const createChatMessage = (roomId, content, url) =>
const createDirectMessage = (pubkey, content) => const createDirectMessage = (pubkey, content) =>
new PublishableEvent(4, {content, tags: [["p", pubkey]]}) new PublishableEvent(4, {content, tags: [["p", pubkey]]})
const createNote = (content, mentions = [], topics = []) => { const createNote = (content, tags = []) =>
// Mentions have to come first so interpolation works new PublishableEvent(1, {content, tags: uniqTags(tagsFromContent(content, tags))})
const tags = doPipe(
[],
[
tags => tags.concat(processMentions(mentions)),
tags => tags.concat(topics.map(t => ["t", t])),
tags => tagsFromContent(content, tags),
uniqTags,
]
)
return new PublishableEvent(1, {content, tags})
}
const createReaction = (note, content) => const createReaction = (note, content) =>
new PublishableEvent(7, {content, tags: getReplyTags(note)}) new PublishableEvent(7, {content, tags: getReplyTags(note)})
const createReply = (note, content, mentions = [], topics = []) => { const createReply = (note, content, tags = []) =>
// Mentions have to come first so interpolation works new PublishableEvent(1, {
const tags = doPipe( content,
[], tags: doPipe(tags, [
[
tags => tags.concat(processMentions(mentions)),
tags => tags.concat(topics.map(t => ["t", t])),
tags => tags.concat(getReplyTags(note, true)), tags => tags.concat(getReplyTags(note, true)),
tags => tagsFromContent(content, tags), tags => tagsFromContent(content, tags),
uniqTags, uniqTags,
] ]),
) })
return new PublishableEvent(1, {content, tags})
}
const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => { const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => {
const tags = [ const tags = [
@ -104,27 +86,22 @@ const deleteEvent = ids => new PublishableEvent(5, {tags: ids.map(id => ["e", id
// Utils // Utils
const processMentions = map(pubkey => {
const name = displayPerson(getPersonWithFallback(pubkey))
const pHint = getRelayForPersonHint(pubkey)
return ["p", pubkey, pHint?.url || "", name]
})
const tagsFromContent = (content, tags) => { const tagsFromContent = (content, tags) => {
const seen = new Set(Tags.wrap(tags).values().all()) const seen = new Set(Tags.wrap(tags).values().all())
for (const {type, value} of parseContent({content})) { for (const {type, value} of parseContent({content})) {
if (type === "topic") {
tags = tags.concat([["t", value]])
seen.add(value)
}
if (type.match(/nostr:(note|nevent)/) && !seen.has(value.id)) { if (type.match(/nostr:(note|nevent)/) && !seen.has(value.id)) {
tags = tags.concat([["e", value.id, "mention"]]) tags = tags.concat([["e", value.id, "mention"]])
seen.add(value.id) seen.add(value.id)
} }
if (type.match(/nostr:(nprofile|npub)/) && !seen.has(value.pubkey)) { if (type.match(/nostr:(nprofile|npub)/) && !seen.has(value.pubkey)) {
const name = displayPerson(getPersonWithFallback(value.pubkey)) tags = tags.concat([mention(value.pubkey)])
const pHint = getRelayForPersonHint(value.pubkey)
tags = tags.concat([["p", value.pubkey, pHint?.url || "", name]])
seen.add(value.pubkey) seen.add(value.pubkey)
} }
} }
@ -132,6 +109,13 @@ const tagsFromContent = (content, tags) => {
return tags return tags
} }
const mention = pubkey => {
const name = displayPerson(getPersonWithFallback(pubkey))
const hint = getRelayForPersonHint(pubkey)
return ["p", pubkey, hint?.url || "", name]
}
const getReplyTags = (n, inherit = false) => { const getReplyTags = (n, inherit = false) => {
const extra = inherit const extra = inherit
? Tags.from(n) ? Tags.from(n)
@ -147,7 +131,7 @@ const getReplyTags = (n, inherit = false) => {
t => t.slice(0, 3).concat("root"), t => t.slice(0, 3).concat("root"),
]) ])
return [["p", n.pubkey, pHint?.url || ""], root, ...extra, reply] return [mention(n.pubkey), root, ...extra, reply]
} }
const uniqTags = uniqBy(t => t.slice(0, 2).join(":")) const uniqTags = uniqBy(t => t.slice(0, 2).join(":"))
@ -185,6 +169,7 @@ class PublishableEvent {
} }
export default { export default {
mention,
authenticate, authenticate,
updateUser, updateUser,
setRelays, setRelays,

View File

@ -197,7 +197,7 @@ export const dropAll = () => new Promise(resolve => loki.deleteDatabase(resolve)
// Domain-specific collections // Domain-specific collections
const sortByCreatedAt = sortBy(e => -e.created_at) const sortByCreatedAt = sortBy(e => -e.created_at)
const sortByLastSeen = sortBy(e => -e.last_seen) const sortByScore = sortBy(e => -e.score)
export const people = new Table("people", "pubkey", { export const people = new Table("people", "pubkey", {
max: 3000, max: 3000,
@ -215,7 +215,7 @@ export const notifications = new Table("notifications", "id", {sort: sortByCreat
export const contacts = new Table("contacts", "pubkey") export const contacts = new Table("contacts", "pubkey")
export const rooms = new Table("rooms", "id") export const rooms = new Table("rooms", "id")
export const relays = new Table("relays", "url") export const relays = new Table("relays", "url")
export const routes = new Table("routes", "id", {max: 3000, sort: sortByLastSeen}) export const routes = new Table("routes", "id", {max: 10000, sort: sortByScore})
export const topics = new Table("topics", "name") export const topics = new Table("topics", "name")
export const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey} export const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}

View File

@ -7,6 +7,7 @@
import LoginPubKey from "src/app/views/LoginPubKey.svelte" import LoginPubKey from "src/app/views/LoginPubKey.svelte"
import Onboarding from "src/app/views/Onboarding.svelte" import Onboarding from "src/app/views/Onboarding.svelte"
import NoteCreate from "src/app/views/NoteCreate.svelte" import NoteCreate from "src/app/views/NoteCreate.svelte"
import NoteDelete from "src/app/views/NoteDelete.svelte"
import NoteZap from "src/app/views/NoteZap.svelte" import NoteZap from "src/app/views/NoteZap.svelte"
import NoteShare from "src/app/views/NoteShare.svelte" import NoteShare from "src/app/views/NoteShare.svelte"
import NoteDetail from "src/app/views/NoteDetail.svelte" import NoteDetail from "src/app/views/NoteDetail.svelte"
@ -29,7 +30,9 @@
<NoteDetail {...m} invertColors /> <NoteDetail {...m} invertColors />
{/key} {/key}
{:else if m.type === "note/create"} {:else if m.type === "note/create"}
<NoteCreate pubkey={m.pubkey} nevent={m.nevent} writeTo={m.relays} /> <NoteCreate pubkey={m.pubkey} quote={m.quote} writeTo={m.relays} />
{:else if m.type === "note/delete"}
<NoteDelete note={m.note} />
{:else if m.type === "note/zap"} {:else if m.type === "note/zap"}
<NoteZap note={m.note} /> <NoteZap note={m.note} />
{:else if m.type === "note/share"} {:else if m.type === "note/share"}

View File

@ -40,9 +40,10 @@
modal.push({type: "note/share", note}) modal.push({type: "note/share", note})
} }
const quote = () => modal.push({type: "note/create", nevent}) const quote = () => modal.push({type: "note/create", quote: note})
const mute = () => user.addMute("e", note.id) const mute = () => user.addMute("e", note.id)
const unmute = () => user.removeMute(note.id) const unmute = () => user.removeMute(note.id)
const deleteSelf = () => modal.push({type: "note/delete", note})
const react = async content => { const react = async content => {
like = first(await cmd.createReaction(note, content).publish(getEventPublishRelays(note))) like = first(await cmd.createReaction(note, content).publish(getEventPublishRelays(note)))
@ -83,6 +84,10 @@
actions.push({label: "Share", icon: "share-nodes", onClick: share}) actions.push({label: "Share", icon: "share-nodes", onClick: share})
actions.push({label: "Quote", icon: "quote-left", onClick: quote}) actions.push({label: "Quote", icon: "quote-left", onClick: quote})
if (note.pubkey === user.getPubkey()) {
actions.push({label: "Delete", icon: "trash", onClick: deleteSelf})
}
if (muted) { if (muted) {
actions.push({label: "Unmute", icon: "microphone", onClick: unmute}) actions.push({label: "Unmute", icon: "microphone", onClick: unmute})
} else { } else {

View File

@ -48,17 +48,15 @@
} }
const send = async () => { const send = async () => {
let {content, mentions, topics} = reply.parse() let content = reply.parse()
if (data.image) { if (data.image) {
content = (content + "\n" + data.image).trim() content = (content + "\n" + data.image).trim()
} }
if (content) { if (content) {
mentions = uniq(mentions.concat(data.mentions))
const relays = getEventPublishRelays(note) const relays = getEventPublishRelays(note)
const thunk = cmd.createReply(note, content, mentions, topics) const thunk = cmd.createReply(note, content, data.mentions.map(cmd.mention))
const [event, promise] = await publishWithToast(relays, thunk) const [event, promise] = await publishWithToast(relays, thunk)
promise.then(({succeeded}) => { promise.then(({succeeded}) => {

View File

@ -15,15 +15,15 @@
import Heading from "src/partials/Heading.svelte" import Heading from "src/partials/Heading.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte" import RelayCard from "src/app/shared/RelayCard.svelte"
import RelaySearch from "src/app/shared/RelaySearch.svelte" import RelaySearch from "src/app/shared/RelaySearch.svelte"
import {getUserWriteRelays} from "src/agent/relays" import {getUserWriteRelays, getRelayForPersonHint} from "src/agent/relays"
import {getPersonWithFallback} from "src/agent/db" import {getPersonWithFallback} from "src/agent/db"
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import user from "src/agent/user" import user from "src/agent/user"
import {toast, modal} from "src/partials/state" import {toast, modal} from "src/partials/state"
import {publishWithToast} from "src/app/state" import {publishWithToast} from "src/app/state"
export let quote = null
export let pubkey = null export let pubkey = null
export let nevent = null
export let writeTo: string[] | null = null export let writeTo: string[] | null = null
let q = "" let q = ""
@ -38,14 +38,23 @@
) )
const onSubmit = async () => { const onSubmit = async () => {
let {content, mentions, topics} = compose.parse() let content = compose.parse()
const tags = []
if (image) { if (image) {
content = content + "\n" + image content = content + "\n" + image
} }
if (quote) {
const {pubkey} = quote
const person = getPersonWithFallback(pubkey)
const pHint = getRelayForPersonHint(pubkey)
tags.push(["p", pubkey, pHint?.url || "", displayPerson(person)])
}
if (content) { if (content) {
const thunk = cmd.createNote(content.trim(), mentions, topics) const thunk = cmd.createNote(content.trim(), tags)
const [event, promise] = await publishWithToast($relays, thunk) const [event, promise] = await publishWithToast($relays, thunk)
promise.then(() => promise.then(() =>
@ -90,7 +99,9 @@
compose.mention(getPersonWithFallback(pubkey)) compose.mention(getPersonWithFallback(pubkey))
} }
if (nevent) { if (quote) {
const nevent = nip19.neventEncode({id: quote.id, relays: [quote.seen_on]})
compose.nevent("nostr:" + nevent) compose.nevent("nostr:" + nevent)
} }
}) })

View File

@ -0,0 +1,24 @@
<script lang="ts">
import {modal} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import {getUserWriteRelays} from "src/agent/relays"
import cmd from "src/agent/cmd"
export let note
const confirm = () => {
cmd.deleteEvent([note.id]).publish(getUserWriteRelays())
modal.pop()
}
</script>
<Content size="lg">
<div class="text-center">
Notes cannot be reliably deleted on nostr, but you can ask. Are you sure you want to delete this
note?
</div>
<div class="flex justify-center">
<Anchor type="button" on:click={confirm}>Confirm</Anchor>
</div>
</Content>

View File

@ -45,7 +45,7 @@
await Promise.all([ await Promise.all([
user.updateRelays(() => user.getRelays()), user.updateRelays(() => user.getRelays()),
cmd.updateUser(profile).publish(user.getRelays()), cmd.updateUser(profile).publish(user.getRelays()),
note && cmd.createNote(note.content, note.mentions, note.topics).publish(user.getRelays()), note && cmd.createNote(note).publish(user.getRelays()),
user.updatePetnames(() => user.updatePetnames(() =>
user.getPetnamePubkeys().map(pubkey => { user.getPetnamePubkeys().map(pubkey => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey)) const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))

View File

@ -203,20 +203,16 @@
export const parse = () => { export const parse = () => {
let {content, annotations} = contenteditable.parse() let {content, annotations} = contenteditable.parse()
const topics = pluck("value", annotations.filter(propEq("prefix", "#")))
// Remove zero-width and non-breaking spaces // Remove zero-width and non-breaking spaces
content = content.replace(/[\u200B\u00A0]/g, " ").trim() content = content.replace(/[\u200B\u00A0]/g, " ").trim()
// We're still using old style mention interpolation until NIP-27 // Strip the @ sign in mentions
// gets merged https://github.com/nostr-protocol/nips/pull/381/files annotations.filter(propEq("prefix", "@")).forEach(({value}, index) => {
const mentions = annotations.filter(propEq("prefix", "@")).map(({value}, index) => { content = content.replace("@" + value, value)
content = content.replace("@" + value, `#[${index}]`)
return pubkeyEncoder.decode(value)
}) })
return {content, topics, mentions} return content
} }
</script> </script>

View File

@ -77,7 +77,9 @@ export class Tags {
} }
export const findReplyAndRoot = e => { export const findReplyAndRoot = e => {
const tags = Tags.from(e).type("e") const tags = Tags.from(e)
.type("e")
.filter(t => last(t) !== "mention")
const legacy = tags.any(t => !["reply", "root"].includes(last(t))) const legacy = tags.any(t => !["reply", "root"].includes(last(t)))
// Support the deprecated version where tags are not marked as replies // Support the deprecated version where tags are not marked as replies