Move mutes over

This commit is contained in:
Jonathan Staab 2023-09-11 12:07:03 -07:00
parent 3e964b5c24
commit 43243dd2c0
20 changed files with 104 additions and 116 deletions

View File

@ -14,7 +14,7 @@
import RelayFeed from "src/app/shared/RelayFeed.svelte"
import Note from "src/app/shared/Note.svelte"
import {getSetting} from "src/engine2"
import {user, Keys, Nip65} from "src/app/engine"
import {Keys, Nip65} from "src/app/engine"
import {compileFilter} from "src/app/state"
export let relays = []
@ -83,7 +83,6 @@
depth: 2,
relays: getRelays(),
filters: [compileFilter(filter)],
isMuted: user.isMuted,
shouldLoadParents: true,
onEvent,
})

View File

@ -13,8 +13,8 @@
import NoteReply from "src/app/shared/NoteReply.svelte"
import NoteActions from "src/app/shared/NoteActions.svelte"
import Card from "src/partials/Card.svelte"
import {getSetting} from "src/engine2"
import {Keys, user, Directory, Nip65, Nip02} from "src/app/engine"
import {getSetting, isMuted} from "src/engine2"
import {Keys, Directory, Nip65, Nip02} from "src/app/engine"
import NoteContent from "src/app/shared/NoteContent.svelte"
export let note
@ -38,7 +38,7 @@
const showEntire = anchorId === note.id
const interactive = !anchorId || !showEntire
const author = Directory.profiles.key(note.pubkey).derived(defaultTo({pubkey: note.pubkey}))
const muted = Nip02.graph.key(Keys.pubkey.get()).derived(() => user.isIgnoring(note.id))
const muted = Nip02.graph.key(Keys.pubkey.get()).derived(() => isMuted(note.id))
let border, childrenContainer, noteContainer

View File

@ -1,20 +1,19 @@
<script>
import {first} from "hurdak"
import {ContextLoader} from "src/engine"
import {onMount, onDestroy} from "svelte"
import {fly} from "src/util/transition"
import Content from "src/partials/Content.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/app/shared/Note.svelte"
import engine, {Settings, Nip65, user, Network} from "src/app/engine"
import {ContextLoader} from "src/engine2"
import engine, {Settings, Nip65, Network} from "src/app/engine"
export let id
export let relays = []
export let invertColors = false
const context = new ContextLoader(engine, {
filter: {ids: [id]},
isMuted: user.isMuted,
filters: [{ids: [id]}],
onEvent: e => {
// Update feed, but only if we have loaded an actual note
if (note) {

View File

@ -9,8 +9,8 @@
import Card from "src/partials/Card.svelte"
import Spinner from "src/partials/Spinner.svelte"
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import {getSetting} from "src/engine2"
import {Directory, Nip65, user, Network} from "src/app/engine"
import {getSetting, isMuted} from "src/engine2"
import {Directory, Nip65, Network} from "src/app/engine"
export let note
export let value
@ -41,7 +41,7 @@
const onEvent = event => {
loading = false
muted = user.applyMutes([event]).length === 0
muted = isMuted(event)
quote = event
}

View File

@ -4,6 +4,7 @@
import {modal} from "src/partials/state"
import Popover from "src/partials/Popover.svelte"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import {mute, unmute, isMuted} from "src/engine2"
import {Env, Keys, user, Nip02} from "src/app/engine"
import {addToList} from "src/app/state"
@ -14,7 +15,7 @@
const npub = nip19.npubEncode(pubkey)
const graphEntry = Nip02.graph.key(Keys.pubkey.get())
const following = graphEntry.derived(() => user.isFollowing(pubkey))
const muted = graphEntry.derived(() => user.isIgnoring(pubkey))
const muted = graphEntry.derived(() => isMuted(pubkey))
let actions = []
@ -73,9 +74,9 @@
const follow = () => user.follow(pubkey)
const unmute = () => user.unmute(pubkey)
const unmutePerson = () => unmute(pubkey)
const mute = () => user.mute("p", pubkey)
const mutePerson = () => mute("p", pubkey)
</script>
<div class="flex items-center gap-3" on:click|stopPropagation>
@ -83,9 +84,9 @@
<Popover triggerType="mouseenter">
<div slot="trigger" class="w-6 text-center">
{#if $muted}
<i class="fa fa-microphone-slash cursor-pointer" on:click={unmute} />
<i class="fa fa-microphone-slash cursor-pointer" on:click={unmutePerson} />
{:else}
<i class="fa fa-microphone cursor-pointer" on:click={mute} />
<i class="fa fa-microphone cursor-pointer" on:click={mutePerson} />
{/if}
</div>
<div slot="tooltip">{$muted ? "Unmute" : "Mute"}</div>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import cx from "classnames"
import {filter, propEq} from "ramda"
import type {DynamicFilter} from "src/engine/types"
import {Tags, noteKinds} from "src/util/nostr"
import {modal, theme} from "src/partials/state"
@ -7,16 +8,17 @@
import Content from "src/partials/Content.svelte"
import Popover from "src/partials/Popover.svelte"
import Feed from "src/app/shared/Feed.svelte"
import {Keys, user, default as engine} from "src/app/engine"
import {stateKey, lists} from "src/engine2"
import {Keys, user} from "src/app/engine"
const lists = engine.Content.lists.derived(() => user.getLists())
const userLists = lists.derived(filter(propEq("pubkey", stateKey.get())))
const showLists = () => modal.push({type: "list/list"})
const showLogin = () => modal.push({type: "login/intro"})
const loadListFeed = naddr => {
const list = engine.Content.lists.key(naddr).get()
const list = lists.key(naddr).get()
const authors = Tags.wrap(list.tags).pubkeys()
const topics = Tags.wrap(list.tags).topics()
const urls = Tags.wrap(list.tags).urls()
@ -25,14 +27,14 @@
relays = urls
}
filter = {kinds: noteKinds, authors: "global"} as DynamicFilter
feedFilter = {kinds: noteKinds, authors: "global"} as DynamicFilter
if (authors.length > 0) {
filter = {...filter, authors}
feedFilter = {...feedFilter, authors}
}
if (topics.length > 0) {
filter = {...filter, "#t": topics}
feedFilter = {...feedFilter, "#t": topics}
}
key = Math.random()
@ -40,7 +42,7 @@
let relays = []
let key = Math.random()
let filter = {
let feedFilter = {
kinds: noteKinds,
authors: user.getFollows().length > 0 ? "follows" : "network",
} as DynamicFilter
@ -58,16 +60,16 @@
</Content>
{/if}
{#key key}
<Feed {filter} {relays}>
<Feed filter={feedFilter} {relays}>
<div slot="controls">
{#if Keys.canSign.get()}
{#if $lists.length > 0}
{#if $userLists.length > 0}
<Popover placement="bottom" opts={{hideOnClick: true}} theme="transparent">
<i slot="trigger" class="fa fa-ellipsis-v cursor-pointer p-2" />
<div
slot="tooltip"
class="flex flex-col items-start overflow-hidden rounded border border-solid border-gray-8 bg-black">
{#each $lists as list (list.naddr)}
{#each $userLists as list (list.naddr)}
<button
class={cx("w-full px-3 py-2 text-left transition-colors", {
"hover:bg-gray-7": $theme === "dark",

View File

@ -8,6 +8,7 @@
import Anchor from "src/partials/Anchor.svelte"
import Input from "src/partials/Input.svelte"
import MultiSelect from "src/partials/MultiSelect.svelte"
import {lists, publishBookmarksList} from "src/engine2"
import {Directory, user, Nip65, default as engine} from "src/app/engine"
export let list
@ -43,7 +44,9 @@
return toast.show("error", "A name is required for your list")
}
const duplicates = user.getLists(l => l.name === values.name && l.naddr !== list?.naddr)
const duplicates = lists.filter(
l => l.pubkey === user.get().pubkey && l.name === values.name && l.naddr !== list?.naddr
)
if (duplicates.length > 0) {
return toast.show("error", "That name is already in use")
@ -51,7 +54,7 @@
const {name, params, relays} = values
user.putList(name, params, relays)
publishBookmarksList(name, [...params, ...relays])
toast.show("info", "Your list has been saved!")
modal.pop()
}

View File

@ -1,12 +1,13 @@
<script type="ts">
import {propEq, filter} from "ramda"
import {modal, appName} from "src/partials/state"
import Heading from "src/partials/Heading.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import ListSummary from "src/app/shared/ListSummary.svelte"
import {default as engine, user} from "src/app/engine"
import {stateKey, lists, publishDeletion} from "src/engine2"
const lists = engine.Content.lists.derived(() => user.getLists())
const userLists = lists.derived(filter(propEq("pubkey", stateKey.get())))
const createFeed = () => {
modal.push({type: "list/edit"})
@ -29,11 +30,11 @@
handing using the "<i class="fa fa-plus" /> List" button above, or by clicking the
<i class="fa fa-scroll px-1" /> icon that appears throughout {appName}.
</p>
{#each $lists as list (list.naddr)}
{#each $userLists as list (list.naddr)}
<div class="flex justify-start gap-3">
<i
class="fa fa-sm fa-trash cursor-pointer py-3"
on:click|stopPropagation={() => user.removeList(list.naddr)} />
on:click|stopPropagation={() => publishDeletion([list.naddr])} />
<div class="flex w-full justify-between">
<div>
<strong>{list.name}</strong>

View File

@ -11,7 +11,8 @@
import Modal from "src/partials/Modal.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/app/shared/Note.svelte"
import {Settings, Nip65, user, Network} from "src/app/engine"
import {Subscription} from "src/engine2"
import {Settings, Nip65} from "src/app/engine"
export let note
export let relays = []
@ -21,10 +22,8 @@
feedRelay = relay
}
const filter = {ids: [note.id]}
const context = new ContextLoader({
filter,
isMuted: user.isMuted,
filters: [{ids: [note.id]}],
onEvent: e => {
// Update feed, but only if we have loaded an actual note
if (displayNote.sig) {
@ -42,17 +41,18 @@
// If our note came from a feed, we can preload context
context.hydrate([displayNote], depth)
sub = Network.subscribe({
filter,
sub = new Subscription({
filters: [{ids: [note.id]}],
timeout: 8000,
relays: Nip65.selectHints(Settings.getSetting("relay_limit"), relays),
onEvent: e => {
})
sub.on("event", e => {
context.addContext([e], {depth})
displayNote = first(context.applyContext([e]))
sub.close()
},
})
await sub.result

View File

@ -6,12 +6,11 @@
import Anchor from "src/partials/Anchor.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Note from "src/app/shared/Note.svelte"
import {user} from "src/app/engine"
export let anchorId
export let relays
const loader = new ThreadLoader({anchorId, relays, isMuted: user.isMuted})
const loader = new ThreadLoader({anchorId, relays})
const {anchor, root, parent, ancestors} = loader
let loading = true

View File

@ -57,8 +57,6 @@ export class Nip02 {
isFollowing = (a: string, b: string) => this.getFollowsSet(a).has(b)
isIgnoring = (a: string, b: string) => this.getMutesSet(a).has(b)
initialize(engine: Engine) {
engine.Events.addHandler(3, e => {
const entry = this.graph.key(e.pubkey).get()

View File

@ -1,9 +1,9 @@
import {nth, when, whereEq, find, reject} from "ramda"
import {nth, when, whereEq, reject} from "ramda"
import {now} from "src/util/misc"
import {appDataKeys, normalizeRelayUrl, findReplyId, findRootId} from "src/util/nostr"
import {appDataKeys, normalizeRelayUrl} from "src/util/nostr"
import {derived} from "src/engine/util/store"
import type {Readable} from "src/engine/util/store"
import type {RelayPolicyEntry, List, Event} from "src/engine/types"
import type {RelayPolicyEntry} from "src/engine/types"
import type {Engine} from "src/engine/Engine"
export class User {
@ -107,9 +107,6 @@ export class User {
isFollowing = (pubkey: string) =>
this.engine.Nip02.isFollowing(this.engine.Keys.stateKey.get(), pubkey)
isIgnoring = (pubkeyOrEventId: string) =>
this.engine.Nip02.isIgnoring(this.engine.Keys.stateKey.get(), pubkeyOrEventId)
setProfile = ($profile: Record<string, any>) =>
this.engine.Outbox.publish({
event: this.engine.Builder.setProfile($profile),
@ -140,54 +137,4 @@ export class User {
unfollow = (pubkey: string) =>
this.setPetnames(reject((t: string[]) => t[1] === pubkey, this.getPetnames()))
isMuted = (e: Event) => {
const m = this.getMutesSet()
return Boolean(find(t => m.has(t), [e.id, e.pubkey, findReplyId(e), findRootId(e)]))
}
applyMutes = (events: Event[]) => reject(this.isMuted, events)
setMutes = async ($mutes: string[][]) => {
if (this.engine.Keys.canSign.get()) {
await this.engine.Outbox.publish({
event: this.engine.Builder.setMutes($mutes.map(t => t.slice(0, 2))),
relays: this.getRelayUrls("write"),
})
} else {
this.engine.Nip02.graph.key(this.engine.Keys.stateKey.get()).merge({
updated_at: now(),
mutes_updated_at: now(),
mutes: $mutes,
})
}
}
mute = (type: string, value: string) =>
this.setMutes(
reject((t: string[]) => t[1] === value, this.getMutedTags()).concat([[type, value]])
)
unmute = (target: string) =>
this.setMutes(reject((t: string[]) => t[1] === target, this.getMutedTags()))
// Lists
getLists = (f?: (l: List) => boolean) =>
this.engine.Content.getLists(
l => l.pubkey === this.engine.Keys.stateKey.get() && (f ? f(l) : true)
)
putList = (name: string, params: string[][], relays: string[]) =>
this.engine.Outbox.publish({
event: this.engine.Builder.createList([["d", name]].concat(params).concat(relays)),
relays: this.getRelayUrls("write"),
})
removeList = (naddr: string) =>
this.engine.Outbox.publish({
event: this.engine.Builder.deleteNaddrs([naddr]),
relays: this.getRelayUrls("write"),
})
}

View File

@ -8,6 +8,7 @@ export * from "./nip24"
export * from "./nip25"
export * from "./nip28"
export * from "./nip32"
export * from "./nip51"
export * from "./nip56"
export * from "./nip57"
export * from "./nip95"

View File

@ -0,0 +1,25 @@
import {reject} from "ramda"
import {now} from "src/util/misc"
import {people} from "src/engine2/state"
import {user, canSign, stateKey} from "src/engine2/queries"
import {updateKey} from "src/engine2/projections"
import {publishEvent} from "./util"
export const publishMutes = async ($mutes: string[][]) => {
if (canSign.get()) {
publishEvent(10000, {tags: $mutes.map(t => t.slice(0, 2))})
} else {
updateKey(people.key(stateKey.get()), now(), {mutes: $mutes})
}
}
export const mute = (type: string, pubkey: string) =>
publishMutes([...reject((t: string[]) => t[1] === pubkey, user.get().mutes), [type, pubkey]])
export const unmute = (value: string) =>
publishMutes(reject((t: string[]) => t[1] === value, user.get().mutes))
export const publishPersonList = (name, tags) => publishEvent(30000, {tags: [["d", name], ...tags]})
export const publishBookmarksList = (name, tags) =>
publishEvent(30001, {tags: [["d", name], ...tags]})

View File

@ -4,6 +4,7 @@ export * from "./settings"
export * from "./nip01"
export * from "./nip02"
export * from "./nip05"
export * from "./nip51"
export * from "./nip57"
export * from "./nip65"
export * from "./alerts"

View File

@ -0,0 +1,10 @@
import {find, nth} from "ramda"
import type {Event} from "src/engine2/model"
import {findReplyId, findRootId} from "src/util/nostr"
import {user} from "src/engine2/queries"
export const isMuted = (e: Event) => {
const m = new Set((user.get().mutes || []).map(nth(1)))
return Boolean(find(t => m.has(t), [e.id, e.pubkey, findReplyId(e), findRootId(e)]))
}

View File

@ -1,7 +1,7 @@
import {find, defaultTo, whereEq} from "ramda"
import type {KeyState} from "src/engine2/model"
import type {KeyState, Person} from "src/engine2/model"
import {derived} from "src/engine2/util/store"
import {pubkey, keys} from "src/engine2/state"
import {pubkey, keys, people} from "src/engine2/state"
import {prepareNdk, ndkInstances} from "./ndk"
import {Signer} from "./signer"
import {Nip04} from "./nip04"
@ -10,8 +10,15 @@ import {Wrapper} from "./wrapper"
export const stateKey = pubkey.derived(defaultTo("anonymous"))
export const user = derived([pubkey, keys], ([$pubkey, $keys]) => {
return find(whereEq({pubkey: $pubkey}), $keys) as KeyState | null
export const user = derived([pubkey, keys, people.mapStore], ([$pubkey, $keys, $people]) => {
if (!$pubkey) {
return null
}
return {
...$people.get($pubkey),
...find(whereEq({pubkey: $pubkey}), $keys),
} as KeyState & Person
})
export const canSign = user.derived(({method}) =>

View File

@ -8,7 +8,7 @@ import {collection} from "src/engine2/util/store"
import type {Collection} from "src/engine2/util/store"
import type {Event, DisplayEvent, Filter} from "src/engine2/model"
import {settings, env} from "src/engine2/state"
import {mergeHints, getReplyHints, getRootHints, getParentHints} from "src/engine2/queries"
import {mergeHints, isMuted, getReplyHints, getRootHints, getParentHints} from "src/engine2/queries"
import {Subscription} from "./subscription"
import {loadPubkeys} from "./pubkeys"
@ -16,7 +16,6 @@ const fromDisplayEvent = (e: DisplayEvent): Event =>
omit(["zaps", "likes", "replies", "matchesFilter"], e)
export type ContextLoaderOpts = {
isMuted: (e: Event) => boolean
relays?: string[]
filters?: Filter[]
onEvent?: (e: Event) => void
@ -76,7 +75,7 @@ export class ContextLoader {
}
preprocessEvents = (events: Event[]) => {
events = reject((e: Event) => this.seen.has(e.id) || this.opts.isMuted(e), events)
events = reject((e: Event) => this.seen.has(e.id) || isMuted(e), events)
for (const event of events) {
this.seen.add(event.id)
@ -143,7 +142,7 @@ export class ContextLoader {
if (substituteParents) {
// We may have loaded a reply from a follower to someone we muted
notes = reject(
this.opts.isMuted,
isMuted,
notes.map(note => {
for (let i = 0; i < 2; i++) {
const parent = contextById[findReplyId(note)]

View File

@ -13,7 +13,6 @@ export type FeedOpts = {
depth: number
relays: string[]
filters: Filter[]
isMuted: (e: Event) => boolean
onEvent?: (e: Event) => void
shouldLoadParents?: boolean
shouldUseNip65?: boolean
@ -36,7 +35,6 @@ export class FeedLoader {
this.context = new ContextLoader({
relays: opts.shouldUseNip65 ? null : urls,
filters: opts.filters,
isMuted: opts.isMuted,
onEvent: event => {
opts.onEvent?.(event)

View File

@ -9,7 +9,6 @@ import {Subscription} from "./subscription"
export type ThreadOpts = {
anchorId: string
relays: string[]
isMuted: (e: Event) => boolean
}
export class ThreadLoader {
@ -22,7 +21,6 @@ export class ThreadLoader {
constructor(readonly opts: ThreadOpts) {
this.context = new ContextLoader({
isMuted: opts.isMuted,
onEvent: this.updateThread,
})