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