From 4ccf12efa8a3d3e405c2fd1c517d06fc0b9907f8 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Wed, 7 Jun 2023 05:52:46 -0700 Subject: [PATCH] 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 --- CHANGELOG.md | 9 +++++ src/agent/cmd.ts | 63 ++++++++++++------------------- src/agent/db.ts | 4 +- src/app/ModalRoutes.svelte | 5 ++- src/app/shared/NoteActions.svelte | 7 +++- src/app/shared/NoteReply.svelte | 6 +-- src/app/views/NoteCreate.svelte | 21 ++++++++--- src/app/views/NoteDelete.svelte | 24 ++++++++++++ src/app/views/Onboarding.svelte | 2 +- src/partials/Compose.svelte | 12 ++---- src/util/nostr.ts | 4 +- 11 files changed, 95 insertions(+), 62 deletions(-) create mode 100644 src/app/views/NoteDelete.svelte diff --git a/CHANGELOG.md b/CHANGELOG.md index b8798c3e..f4692c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/agent/cmd.ts b/src/agent/cmd.ts index f79edf8a..3e2425be 100644 --- a/src/agent/cmd.ts +++ b/src/agent/cmd.ts @@ -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, diff --git a/src/agent/db.ts b/src/agent/db.ts index 301d5ba5..52400283 100644 --- a/src/agent/db.ts +++ b/src/agent/db.ts @@ -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} diff --git a/src/app/ModalRoutes.svelte b/src/app/ModalRoutes.svelte index d225461a..8e8a03fe 100644 --- a/src/app/ModalRoutes.svelte +++ b/src/app/ModalRoutes.svelte @@ -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 @@ {/key} {:else if m.type === "note/create"} - + +{:else if m.type === "note/delete"} + {:else if m.type === "note/zap"} {:else if m.type === "note/share"} diff --git a/src/app/shared/NoteActions.svelte b/src/app/shared/NoteActions.svelte index 82214090..cbebbea1 100644 --- a/src/app/shared/NoteActions.svelte +++ b/src/app/shared/NoteActions.svelte @@ -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 { diff --git a/src/app/shared/NoteReply.svelte b/src/app/shared/NoteReply.svelte index d3351cc0..390bac59 100644 --- a/src/app/shared/NoteReply.svelte +++ b/src/app/shared/NoteReply.svelte @@ -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}) => { diff --git a/src/app/views/NoteCreate.svelte b/src/app/views/NoteCreate.svelte index cbad6a38..59dd2262 100644 --- a/src/app/views/NoteCreate.svelte +++ b/src/app/views/NoteCreate.svelte @@ -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) } }) diff --git a/src/app/views/NoteDelete.svelte b/src/app/views/NoteDelete.svelte new file mode 100644 index 00000000..7a9dd3c0 --- /dev/null +++ b/src/app/views/NoteDelete.svelte @@ -0,0 +1,24 @@ + + + +
+ Notes cannot be reliably deleted on nostr, but you can ask. Are you sure you want to delete this + note? +
+
+ Confirm +
+
diff --git a/src/app/views/Onboarding.svelte b/src/app/views/Onboarding.svelte index 91268efa..d135acbf 100644 --- a/src/app/views/Onboarding.svelte +++ b/src/app/views/Onboarding.svelte @@ -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)) diff --git a/src/partials/Compose.svelte b/src/partials/Compose.svelte index 7a5e78b8..8c893ab9 100644 --- a/src/partials/Compose.svelte +++ b/src/partials/Compose.svelte @@ -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 } diff --git a/src/util/nostr.ts b/src/util/nostr.ts index 7aefe46f..d3aeaa67 100644 --- a/src/util/nostr.ts +++ b/src/util/nostr.ts @@ -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