aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdrien Hopkins <[email protected]>2026-01-09 09:44:24 -0500
committerAdrien Hopkins <[email protected]>2026-01-09 09:44:24 -0500
commitb4d54e3eb924bb51f3f1e27e51b38f58480d5763 (patch)
tree3663283498d2907aba678600c9ff21effef8677d
parent9175c76d7f03c0c62323202fdab3e4ab35854855 (diff)
downloadcoinos-ui-b4d54e3eb924bb51f3f1e27e51b38f58480d5763.tar.gz
coinos-ui-b4d54e3eb924bb51f3f1e27e51b38f58480d5763.zip
Add button to mute/unmute account
-rw-r--r--src/lib/nip07.ts11
-rw-r--r--src/lib/nip17.ts2
-rw-r--r--src/lib/nip51.ts35
-rw-r--r--src/routes/(app)/dm/+page.svelte79
4 files changed, 91 insertions, 36 deletions
diff --git a/src/lib/nip07.ts b/src/lib/nip07.ts
new file mode 100644
index 00000000..b93779dc
--- /dev/null
+++ b/src/lib/nip07.ts
@@ -0,0 +1,11 @@
+import { finalizeEvent } from 'nostr-tools/pure';
+import { getPrivateKey } from '$lib/nostr';
+
+export const signEvent = async (event: object, user: object): object => {
+ if (await window.nostr.getPublicKey() === user.pubkey) {
+ return window.nostr.signEvent(event);
+ } else {
+ const sk = await getPrivateKey();
+ return finalizeEvent(event, sk);
+ }
+}
diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts
index 1a78eb5a..ce5654cf 100644
--- a/src/lib/nip17.ts
+++ b/src/lib/nip17.ts
@@ -4,7 +4,7 @@ import { finalizeEvent, getEventHash, generateSecretKey, getPublicKey } from 'no
import { SimplePool } from 'nostr-tools/pool';
import { unwrapEvent } from 'nostr-tools/nip17';
-import { sign, getPrivateKey } from '$lib/nostr';
+import { getPrivateKey } from '$lib/nostr';
import { relaysSupporting } from '$lib/nip11';
import { expired, expiration } from '$lib/nip40';
import { encrypt, u } from '$lib/nip44';
diff --git a/src/lib/nip51.ts b/src/lib/nip51.ts
index 142f0bbf..521ba60a 100644
--- a/src/lib/nip51.ts
+++ b/src/lib/nip51.ts
@@ -1,16 +1,27 @@
import { SimplePool } from 'nostr-tools/pool';
+import { verifyEvent } from 'nostr-tools/pure';
+import { signEvent } from '$lib/nip07';
const pool = new SimplePool();
import { PUBLIC_DM_RELAYS } from '$env/static/public';
const DM_RELAYS_LIST = PUBLIC_DM_RELAYS.split(',');
-// export const mute = async (pubkey: string, hidden: bool) => {
-
-// }
+export const mute = async (user: object, pubkey: string, hide: bool) => {
+ let { shown, hidden } = await getMuteLists(user);
+ if (hide) {
+ hidden.push(pubkey);
+ } else {
+ shown.push(pubkey);
+ }
+ await publishMuteList(user, shown, hidden);
+}
-// export const unmute = async (pubkey: string) => {
-
-// }
+export const unmute = async (user: object, pubkey: string) => {
+ let { shown, hidden } = await getMuteLists(user);
+ shown = shown.filter(p => p !== pubkey);
+ hidden = hidden.filter(p => p !== pubkey);
+ await publishMuteList(user, shown, hidden);
+}
export const mutedAccounts = async (user: object): Set<string> => {
const { shown, hidden } = await getMuteLists(user);
@@ -42,6 +53,12 @@ const getMuteLists = async (user: object): object => {
return { shown, hidden: [] };
}
-// const publishMuteList = async (shown: Array<string>, hidden: Array<string>) => {
-
-// }
+const publishMuteList = async (user: object, shown: Array<string>, hidden: Array<string>) => {
+ const event = await signEvent({
+ kind: 10000,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: shown.map(p => ["p", p]),
+ content: ''
+ }, user);
+ return Promise.any(pool.publish(DM_RELAYS_LIST, event));
+}
diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte
index 336733e0..a8da9538 100644
--- a/src/routes/(app)/dm/+page.svelte
+++ b/src/routes/(app)/dm/+page.svelte
@@ -10,7 +10,7 @@
import { getMessageRumours } from '$lib/nip17';
import * as libnip17 from '$lib/nip17';
import { npubEncode, decode } from 'nostr-tools/nip19';
- import { mutedAccounts } from '$lib/nip51';
+ import { mute, unmute, mutedAccounts } from '$lib/nip51';
import { sign, getPrivateKey, pTagKeys } from '$lib/nostr';
import { t } from "$lib/translations";
import { theme } from "$lib/store";
@@ -18,9 +18,6 @@
let { data } = $props();
const { user } = data;
- let muted = $state(new Set<string>());
- mutedAccounts(user).then((m) => muted = m);
-
/// chat selection
const usernameFromPubkey = async (pubkey: string): string => {
const response = await fetch(`/api/users/${pubkey}`);
@@ -260,6 +257,18 @@
}
relayWarningShown = false;
});
+
+ let muted = $state(new Set<string>());
+ mutedAccounts(user).then((m) => muted = m);
+
+ const toggleMute = async (pubkey: string) => {
+ if (muted && muted.has(pubkey)) {
+ await unmute(user, pubkey);
+ } else {
+ await mute(user, pubkey, false);
+ }
+ muted = await mutedAccounts(user);
+ }
</script>
<style>
@@ -329,6 +338,12 @@
width: 100%;
}
+ .small-btn {
+ padding: 0 15px;
+ border-radius: 5px;
+ text-align: left;
+ }
+
.push-right {
margin-left: auto;
}
@@ -337,6 +352,14 @@
margin: 1em 15px 0.5em;
}
+ .light-chat-btn {
+ background-color: #eeeeee;
+ }
+
+ .dark-chat-btn {
+ background-color: #222222;
+ }
+
.light-chat-btn:hover {
background-color: #dddddd;
}
@@ -442,35 +465,39 @@
<span class="text-3xl">{name(selectedChat)}</span>
{/if}
<span class={"secondary timestamp text-small" + (idValid(selectedChat) ? "" : " invalid")} title="{npubEncode(selectedChat.pubkey)}">{id(selectedChat)}</span>
- <!-- <button class="push-right">{$t(muted && muted.has(selectedChat.pubkey) ? "dm.unmute" : "dm.mute")}</button> -->
+ <button class={"small-btn push-right " + ($theme === "light" ? "light-chat-btn" : "dark-chat-btn")} on:click={() => toggleMute(selectedChat.pubkey)}>{$t(muted && muted.has(selectedChat.pubkey) ? "dm.unmute" : "dm.mute")}</button>
</h1>
- <div id="messages" style="overflow-y: scroll; max-height: 600px;">
- {#each dates as day}
- <p class="date-header secondary">{formatDate(new Date(day * 86400 * 1000))}</p>
- <ul>
- {#each messageRumours.get(day) as rumour}
- {#if rumour.pubkey === user.pubkey && pTagKeys(rumour).includes(selectedChat.pubkey)}
- <li class={(rumour.expiration && expirationClose(rumour.expiration) ? "secondary " : "") + ($theme === "light" ? "light-message " : "dark-message ") + "message by-user"}>{rumour.content} <span class="timestamp secondary text-xs">{TIME.format(new Date(rumour.created_at * 1000))}</span></li>
- {:else if rumour.pubkey === selectedChat.pubkey && pTagKeys(rumour).includes(user.pubkey)}
- <li class={(rumour.expiration && expirationClose(rumour.expiration) ? "secondary " : "") + ($theme === "light" ? "light-message " : "dark-message ") + "message by-other"}>{rumour.content} <span class="timestamp secondary text-xs">{TIME.format(new Date(rumour.created_at * 1000))}</span></li>
- {/if}
- {/each}
- </ul>
- {/each}
- </div>
+ {#if !(muted && muted.has(selectedChat.pubkey))}
+ <div id="messages" style="overflow-y: scroll; max-height: 600px;">
+ {#each dates as day}
+ <p class="date-header secondary">{formatDate(new Date(day * 86400 * 1000))}</p>
+ <ul>
+ {#each messageRumours.get(day) as rumour}
+ {#if rumour.pubkey === user.pubkey && pTagKeys(rumour).includes(selectedChat.pubkey)}
+ <li class={(rumour.expiration && expirationClose(rumour.expiration) ? "secondary " : "") + ($theme === "light" ? "light-message " : "dark-message ") + "message by-user"}>{rumour.content} <span class="timestamp secondary text-xs">{TIME.format(new Date(rumour.created_at * 1000))}</span></li>
+ {:else if rumour.pubkey === selectedChat.pubkey && pTagKeys(rumour).includes(user.pubkey)}
+ <li class={(rumour.expiration && expirationClose(rumour.expiration) ? "secondary " : "") + ($theme === "light" ? "light-message " : "dark-message ") + "message by-other"}>{rumour.content} <span class="timestamp secondary text-xs">{TIME.format(new Date(rumour.created_at * 1000))}</span></li>
+ {/if}
+ {/each}
+ </ul>
+ {/each}
+ </div>
- <textarea id="message-contents" bind:value={text}></textarea>
- <input id="send-message" type="button" class="btn btn-accent" disabled={!canSend} value={canSend ? $t("dm.sendMessage") : $t("dm.relaysNotFound").replace("[R]", name(selectedChat))} on:click={async () => sendMessage(text)}>
+ <textarea id="message-contents" bind:value={text}></textarea>
+ <input id="send-message" type="button" class="btn btn-accent" disabled={!canSend} value={canSend ? $t("dm.sendMessage") : $t("dm.relaysNotFound").replace("[R]", name(selectedChat))} on:click={async () => sendMessage(text)}>
+ {/if}
{#if relayWarningShown}
<p class="warning"><em>{$t("dm.noRelaysSet")} <a class="link" href="/settings/nostr#dm-relays">{$t("dm.nostrSettingsLink")}</a></em></p>
{/if}
- <input type="checkbox" id="expiryCheckbox" class="tiny" bind:checked={expiryEnabled} disabled={!canSendExpiring}>
- <label for="expiryCheckbox">{$t("dm.expiry")}</label>: <input type="number" class="short small-width vcenter" bind:value={expiryDays} disabled={!expiryEnabled} min="1" step="1" max="99999"> <label for="expiryCheckbox">{$t("dm.days")}</label>.
- <p class="secondary">{$t("dm.fadedWarning")}</p>
- {#if canSend && !canSendExpiring}
- <p class="warning"><em>{name(selectedChat)}</em></p>
+ {#if !(muted && muted.has(selectedChat.pubkey))}
+ <input type="checkbox" id="expiryCheckbox" class="tiny" bind:checked={expiryEnabled} disabled={!canSendExpiring}>
+ <label for="expiryCheckbox">{$t("dm.expiry")}</label>: <input type="number" class="short small-width vcenter" bind:value={expiryDays} disabled={!expiryEnabled} min="1" step="1" max="99999"> <label for="expiryCheckbox">{$t("dm.days")}</label>.
+ <p class="secondary">{$t("dm.fadedWarning")}</p>
+ {#if canSend && !canSendExpiring}
+ <p class="warning"><em>{name(selectedChat)}</em></p>
+ {/if}
{/if}
{/if}
</div>