diff --git a/.changeset/beige-falcons-shake.md b/.changeset/beige-falcons-shake.md
new file mode 100644
index 000000000..397840e74
--- /dev/null
+++ b/.changeset/beige-falcons-shake.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Display NIP-89 client tags on events
diff --git a/dockerfile b/dockerfile
index 87cc75385..c6220fdd2 100644
--- a/dockerfile
+++ b/dockerfile
@@ -13,6 +13,7 @@ COPY . .
ENV VITE_COMMIT_HASH=""
ENV VITE_APP_VERSION="custom"
+ENV ENABLE_CLIENT_TAG="false"
RUN yarn build
FROM nginx:stable-alpine-slim AS main
diff --git a/src/components/note/note-published-using.tsx b/src/components/note/note-published-using.tsx
new file mode 100644
index 000000000..52af54374
--- /dev/null
+++ b/src/components/note/note-published-using.tsx
@@ -0,0 +1,13 @@
+import { Text } from "@chakra-ui/react";
+import { NostrEvent } from "nostr-tools";
+
+export default function NotePublishedUsing({ event }: { event: NostrEvent }) {
+ const clientTag = event.tags.find((t) => t[0] === "client");
+ if (!clientTag) return;
+
+ return (
+
+ using {clientTag[1]}
+
+ );
+}
diff --git a/src/components/note/timeline-note/index.tsx b/src/components/note/timeline-note/index.tsx
index 2388085f9..b66428560 100644
--- a/src/components/note/timeline-note/index.tsx
+++ b/src/components/note/timeline-note/index.tsx
@@ -47,6 +47,7 @@ import ZapBubbles from "./components/zap-bubbles";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import relayHintService from "../../../services/event-relay-hint";
import localSettings from "../../../services/local-settings";
+import NotePublishedUsing from "../note-published-using";
export type TimelineNoteProps = Omit & {
event: NostrEvent;
@@ -108,6 +109,7 @@ export function TimelineNote({
+
{showSignatureVerification && }
{!hideDrawerButton && (
diff --git a/src/const.ts b/src/const.ts
index 7025c4084..820dc35cb 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -1,3 +1,4 @@
+import { kinds } from "nostr-tools";
import { safeRelayUrl, safeRelayUrls } from "./helpers/relay";
export const DEFAULT_SEARCH_RELAYS = safeRelayUrls([
@@ -45,3 +46,11 @@ export const NOSTR_CONNECT_PERMISSIONS = [
"sign_event:6",
"sign_event:7",
];
+
+export const NEVER_ATTACH_CLIENT_TAG = [kinds.EncryptedDirectMessage];
+export const ENABLE_CLIENT_TAG = import.meta.env.VITE_ENABLE_CLIENT_TAG !== "false";
+export const NIP_89_CLIENT_TAG = [
+ "client",
+ "noStrudel",
+ "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:1686066542546",
+];
diff --git a/src/providers/global/publish-provider.tsx b/src/providers/global/publish-provider.tsx
index 262293804..16f677c83 100644
--- a/src/providers/global/publish-provider.tsx
+++ b/src/providers/global/publish-provider.tsx
@@ -13,6 +13,8 @@ import eventReactionsService from "../../services/event-reactions";
import { localRelay } from "../../services/local-relay";
import deleteEventService from "../../services/delete-events";
import userMailboxesService from "../../services/user-mailboxes";
+import localSettings from "../../services/local-settings";
+import { NEVER_ATTACH_CLIENT_TAG, NIP_89_CLIENT_TAG } from "../../const";
type PublishContextType = {
log: PublishAction[];
@@ -78,7 +80,16 @@ export default function PublishProvider({ children }: PropsWithChildren) {
let signed: NostrEvent;
if (!Object.hasOwn(event, "sig")) {
let draft: EventTemplate = event as EventTemplate;
+
+ // add pubkey relay hints
draft = userMailboxesService.addPubkeyRelayHints(draft);
+
+ // add client tag
+ if (localSettings.addClientTag.value && !NEVER_ATTACH_CLIENT_TAG.includes(event.kind)) {
+ draft.tags = [...draft.tags.filter((t) => t[0] !== "client"), NIP_89_CLIENT_TAG];
+ }
+
+ // request signature
signed = await requestSignature(draft);
} else signed = event as NostrEvent;
diff --git a/src/services/local-settings.ts b/src/services/local-settings.ts
index 29d07791b..ce616148c 100644
--- a/src/services/local-settings.ts
+++ b/src/services/local-settings.ts
@@ -1,8 +1,7 @@
import { generateSecretKey } from "nostr-tools";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
-import { PersistentSubject } from "../classes/subject";
-import { DEFAULT_SIGNAL_RELAYS } from "../const";
+import { ENABLE_CLIENT_TAG, DEFAULT_SIGNAL_RELAYS } from "../const";
import {
BooleanLocalStorageEntry,
NullableNumberLocalStorageEntry,
@@ -45,6 +44,9 @@ const webRtcRecentConnections = new LocalStorageEntry(
(value) => value.join(","),
);
+// posting
+const addClientTag = new BooleanLocalStorageEntry("add-client-tag", ENABLE_CLIENT_TAG);
+
const localSettings = {
idbMaxEvents,
wasmPersistForDays,
@@ -53,6 +55,7 @@ const localSettings = {
webRtcLocalIdentity,
webRtcSignalingRelays,
webRtcRecentConnections,
+ addClientTag,
};
if (import.meta.env.DEV) {
diff --git a/src/views/settings/post/index.tsx b/src/views/settings/post/index.tsx
index 9d664bd85..cb568b070 100644
--- a/src/views/settings/post/index.tsx
+++ b/src/views/settings/post/index.tsx
@@ -19,6 +19,7 @@ import {
AlertTitle,
AlertDescription,
Heading,
+ Switch,
} from "@chakra-ui/react";
import { matchSorter } from "match-sorter";
@@ -28,6 +29,8 @@ import useUsersMediaServers from "../../../hooks/use-user-media-servers";
import useCurrentAccount from "../../../hooks/use-current-account";
import useSettingsForm from "../use-settings-form";
import VerticalPageLayout from "../../../components/vertical-page-layout";
+import localSettings from "../../../services/local-settings";
+import useSubject from "../../../hooks/use-subject";
export default function PostSettings() {
const account = useCurrentAccount();
@@ -64,6 +67,8 @@ export default function PostSettings() {
);
};
+ const addClientTag = useSubject(localSettings.addClientTag);
+
return (
Post Settings
@@ -158,6 +163,26 @@ export default function PostSettings() {
How much Proof of work to mine when writing notes. setting this to 0 will disable it
+
+
+
+
+ Add client tag
+
+ localSettings.addClientTag.next(!localSettings.addClientTag.value)}
+ />
+
+
+ Enabled: Attach the{" "}
+
+ NIP-89
+ {" "}
+ client tag to events
+
+
- ) : (
- : }
- aria-label={expanded.isOpen ? "Collapse" : "Expand"}
- title={expanded.isOpen ? "Collapse" : "Expand"}
- />
- )}
+
+
+
+ {!isFocused &&
+ (replies.length > 0 ? (
+ : }>
+ ({numberOfReplies})
+
+ ) : (
+ : }
+ aria-label={expanded.isOpen ? "Collapse" : "Expand"}
+ title={expanded.isOpen ? "Collapse" : "Expand"}
+ />
+ ))}
);
@@ -153,7 +158,7 @@ function ThreadPost({ post, initShowReplies, focusId, level = -1 }: ThreadItemPr
)}
{replyForm.isOpen && }
- {level === -1 ? (
+ {isFocused ? (
) : (
expanded.isOpen &&