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
- [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

View File

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

View File

@ -197,7 +197,7 @@ export const dropAll = () => new Promise(resolve => loki.deleteDatabase(resolve)
// Domain-specific collections
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", {
max: 3000,
@ -215,7 +215,7 @@ export const notifications = new Table("notifications", "id", {sort: sortByCreat
export const contacts = new Table("contacts", "pubkey")
export const rooms = new Table("rooms", "id")
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 getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}

View File

@ -7,6 +7,7 @@
import LoginPubKey from "src/app/views/LoginPubKey.svelte"
import Onboarding from "src/app/views/Onboarding.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 NoteShare from "src/app/views/NoteShare.svelte"
import NoteDetail from "src/app/views/NoteDetail.svelte"
@ -29,7 +30,9 @@
<NoteDetail {...m} invertColors />
{/key}
{: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"}
<NoteZap note={m.note} />
{:else if m.type === "note/share"}

View File

@ -40,9 +40,10 @@
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 unmute = () => user.removeMute(note.id)
const deleteSelf = () => modal.push({type: "note/delete", note})
const react = async content => {
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: "Quote", icon: "quote-left", onClick: quote})
if (note.pubkey === user.getPubkey()) {
actions.push({label: "Delete", icon: "trash", onClick: deleteSelf})
}
if (muted) {
actions.push({label: "Unmute", icon: "microphone", onClick: unmute})
} else {

View File

@ -48,17 +48,15 @@
}
const send = async () => {
let {content, mentions, topics} = reply.parse()
let content = reply.parse()
if (data.image) {
content = (content + "\n" + data.image).trim()
}
if (content) {
mentions = uniq(mentions.concat(data.mentions))
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)
promise.then(({succeeded}) => {

View File

@ -15,15 +15,15 @@
import Heading from "src/partials/Heading.svelte"
import RelayCard from "src/app/shared/RelayCard.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 cmd from "src/agent/cmd"
import user from "src/agent/user"
import {toast, modal} from "src/partials/state"
import {publishWithToast} from "src/app/state"
export let quote = null
export let pubkey = null
export let nevent = null
export let writeTo: string[] | null = null
let q = ""
@ -38,14 +38,23 @@
)
const onSubmit = async () => {
let {content, mentions, topics} = compose.parse()
let content = compose.parse()
const tags = []
if (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) {
const thunk = cmd.createNote(content.trim(), mentions, topics)
const thunk = cmd.createNote(content.trim(), tags)
const [event, promise] = await publishWithToast($relays, thunk)
promise.then(() =>
@ -90,7 +99,9 @@
compose.mention(getPersonWithFallback(pubkey))
}
if (nevent) {
if (quote) {
const nevent = nip19.neventEncode({id: quote.id, relays: [quote.seen_on]})
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([
user.updateRelays(() => 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.getPetnamePubkeys().map(pubkey => {
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))

View File

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

View File

@ -77,7 +77,9 @@ export class Tags {
}
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)))
// Support the deprecated version where tags are not marked as replies