mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 16:07:15 +02:00
feat: add client tag support to all event creation
Implements a global settings system to control whether the Grimoire client tag
should be included in all published events. This allows users to opt-in or
opt-out of identifying their client in published events.
Changes:
- Created global settings service (src/services/settings.ts) with reactive
BehaviorSubject for app-wide configuration
- Created useSettings hook (src/hooks/useSettings.ts) for React components
- Migrated PostViewer from local settings to global settings system
- Added client tag support to:
- Post publishing (PostViewer.tsx)
- Spell publishing (publish-spell.ts)
- Event deletion (delete-event.ts)
- NIP-29 chat messages, reactions, join/leave, and group bookmarks
(nip-29-adapter.ts)
- Zap requests (create-zap-request.ts)
The client tag setting defaults to enabled (true) for backward compatibility.
Users can toggle this in the post composer settings dropdown.
All event creation locations now check settingsManager.getSetting("includeClientTag")
before adding the GRIMOIRE_CLIENT_TAG to event tags.
This commit is contained in:
@@ -7,6 +7,8 @@ import { mergeRelaySets } from "applesauce-core/helpers";
|
||||
import { grimoireStateAtom } from "@/core/state";
|
||||
import { getDefaultStore } from "jotai";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { settingsManager } from "@/services/settings";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
|
||||
export class DeleteEventAction {
|
||||
type = "delete-event";
|
||||
@@ -27,6 +29,12 @@ export class DeleteEventAction {
|
||||
const factory = new EventFactory({ signer });
|
||||
|
||||
const draft = await factory.delete([item.event], reason);
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
draft.tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
const event = await factory.sign(draft);
|
||||
|
||||
// Get write relays from cache and state
|
||||
|
||||
@@ -9,6 +9,8 @@ import { relayListCache } from "@/services/relay-list-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { mergeRelaySets } from "applesauce-core/helpers";
|
||||
import eventStore from "@/services/event-store";
|
||||
import { settingsManager } from "@/services/settings";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
|
||||
export class PublishSpellAction {
|
||||
type = "publish-spell";
|
||||
@@ -40,12 +42,18 @@ export class PublishSpellAction {
|
||||
|
||||
const factory = new EventFactory({ signer });
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
const tags = [...encoded.tags];
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
const draft = await factory.build({
|
||||
kind: 777,
|
||||
|
||||
content: encoded.content,
|
||||
|
||||
tags: encoded.tags,
|
||||
tags,
|
||||
});
|
||||
|
||||
event = (await factory.sign(draft)) as SpellEvent;
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useProfileSearch } from "@/hooks/useProfileSearch";
|
||||
import { useEmojiSearch } from "@/hooks/useEmojiSearch";
|
||||
import { useBlossomUpload } from "@/hooks/useBlossomUpload";
|
||||
import { useRelayState } from "@/hooks/useRelayState";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { RichEditor, type RichEditorHandle } from "./editor/RichEditor";
|
||||
import type { BlobAttachment, EmojiTag } from "./editor/MentionEditor";
|
||||
import { RelayLink } from "./nostr/RelayLink";
|
||||
@@ -53,15 +54,6 @@ interface RelayPublishState {
|
||||
|
||||
// Storage keys
|
||||
const DRAFT_STORAGE_KEY = "grimoire-post-draft";
|
||||
const SETTINGS_STORAGE_KEY = "grimoire-post-settings";
|
||||
|
||||
interface PostSettings {
|
||||
includeClientTag: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: PostSettings = {
|
||||
includeClientTag: true,
|
||||
};
|
||||
|
||||
interface PostViewerProps {
|
||||
windowId?: string;
|
||||
@@ -73,6 +65,7 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
|
||||
const { searchEmojis } = useEmojiSearch();
|
||||
const { state } = useGrimoire();
|
||||
const { getRelay } = useRelayState();
|
||||
const { settings, updateSetting } = useSettings();
|
||||
|
||||
// Editor ref for programmatic control
|
||||
const editorRef = useRef<RichEditorHandle>(null);
|
||||
@@ -86,36 +79,9 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
|
||||
const [showPublishedPreview, setShowPublishedPreview] = useState(false);
|
||||
const [newRelayInput, setNewRelayInput] = useState("");
|
||||
|
||||
// Load settings from localStorage
|
||||
const [settings, setSettings] = useState<PostSettings>(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : DEFAULT_SETTINGS;
|
||||
} catch {
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
});
|
||||
|
||||
// Get relay pool state for connection status
|
||||
const relayPoolMap = use$(pool.relays$);
|
||||
|
||||
// Persist settings to localStorage when they change
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch (err) {
|
||||
console.error("Failed to save post settings:", err);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
// Update a single setting
|
||||
const updateSetting = useCallback(
|
||||
<K extends keyof PostSettings>(key: K, value: PostSettings[K]) => {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Get active account's write relays from Grimoire state, fallback to aggregators
|
||||
const writeRelays = useMemo(() => {
|
||||
if (!state.activeAccount?.relays) return AGGREGATOR_RELAYS;
|
||||
@@ -403,7 +369,7 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
|
||||
}
|
||||
|
||||
// Add client tag (if enabled)
|
||||
if (settings.includeClientTag) {
|
||||
if (settings?.includeClientTag) {
|
||||
additionalTags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
@@ -692,7 +658,7 @@ export function PostViewer({ windowId }: PostViewerProps = {}) {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={settings.includeClientTag}
|
||||
checked={settings?.includeClientTag ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSetting("includeClientTag", checked)
|
||||
}
|
||||
|
||||
33
src/hooks/useSettings.ts
Normal file
33
src/hooks/useSettings.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* React hook for accessing and updating global app settings
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { use$ } from "applesauce-react/hooks";
|
||||
import { settingsManager, type AppSettings } from "@/services/settings";
|
||||
|
||||
export function useSettings() {
|
||||
const settings = use$(settingsManager.stream$);
|
||||
|
||||
const updateSetting = useCallback(
|
||||
<K extends keyof AppSettings>(key: K, value: AppSettings[K]) => {
|
||||
settingsManager.updateSetting(key, value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const updateSettings = useCallback((updates: Partial<AppSettings>) => {
|
||||
settingsManager.updateSettings(updates);
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
settingsManager.reset();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
settings,
|
||||
updateSetting,
|
||||
updateSettings,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
GroupMessageBlueprint,
|
||||
ReactionBlueprint,
|
||||
} from "applesauce-common/blueprints";
|
||||
import { settingsManager } from "@/services/settings";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
|
||||
/**
|
||||
* NIP-29 Adapter - Relay-Based Groups
|
||||
@@ -471,6 +473,11 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
draft.tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const event = await factory.sign(draft);
|
||||
|
||||
@@ -528,6 +535,11 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
// Add h-tag for group context (NIP-29 specific)
|
||||
draft.tags.push(["h", groupId]);
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
draft.tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const event = await factory.sign(draft);
|
||||
|
||||
@@ -868,6 +880,11 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
["relay", relayUrl],
|
||||
];
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
const draft = await factory.build({
|
||||
kind: 9021,
|
||||
content: "",
|
||||
@@ -904,6 +921,11 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
["relay", relayUrl],
|
||||
];
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
const draft = await factory.build({
|
||||
kind: 9022,
|
||||
content: "",
|
||||
@@ -984,6 +1006,11 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
// Add the new group tag (use normalized URL for consistency)
|
||||
tags.push(["group", groupId, normalizedRelayUrl]);
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
// Create and publish the updated event
|
||||
const factory = new EventFactory();
|
||||
factory.setSigner(activeSigner);
|
||||
@@ -1040,6 +1067,11 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
throw new Error("Group is not in your list");
|
||||
}
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
// Create and publish the updated event
|
||||
const factory = new EventFactory();
|
||||
factory.setSigner(activeSigner);
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EventPointer, AddressPointer } from "./open-parser";
|
||||
import accountManager from "@/services/accounts";
|
||||
import { selectZapRelays } from "./zap-relay-selection";
|
||||
import { settingsManager } from "@/services/settings";
|
||||
import { GRIMOIRE_CLIENT_TAG } from "@/constants/app";
|
||||
|
||||
export interface EmojiTag {
|
||||
shortcode: string;
|
||||
@@ -128,6 +130,11 @@ export async function createZapRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// Add client tag if enabled in settings
|
||||
if (settingsManager.getSetting("includeClientTag")) {
|
||||
tags.push(GRIMOIRE_CLIENT_TAG);
|
||||
}
|
||||
|
||||
// Create event template
|
||||
const template = {
|
||||
kind: 9734,
|
||||
|
||||
113
src/services/settings.ts
Normal file
113
src/services/settings.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Global application settings
|
||||
* Manages user preferences with localStorage persistence
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
export interface AppSettings {
|
||||
/** Whether to include client tag in published events */
|
||||
includeClientTag: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: AppSettings = {
|
||||
includeClientTag: true,
|
||||
};
|
||||
|
||||
const SETTINGS_STORAGE_KEY = "grimoire-settings";
|
||||
|
||||
/**
|
||||
* Load settings from localStorage with error handling
|
||||
*/
|
||||
function loadSettings(): AppSettings {
|
||||
try {
|
||||
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return { ...DEFAULT_SETTINGS, ...parsed };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load settings:", err);
|
||||
}
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save settings to localStorage with error handling
|
||||
*/
|
||||
function saveSettings(settings: AppSettings): void {
|
||||
try {
|
||||
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||
} catch (err) {
|
||||
console.error("Failed to save settings:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global settings manager
|
||||
* Use settings$ to reactively observe settings changes
|
||||
* Use getSetting() for non-reactive access
|
||||
* Use updateSetting() to update a setting
|
||||
*/
|
||||
class SettingsManager {
|
||||
private settings$ = new BehaviorSubject<AppSettings>(loadSettings());
|
||||
|
||||
/**
|
||||
* Observable stream of settings
|
||||
* Subscribe to get notified of changes
|
||||
*/
|
||||
get stream$() {
|
||||
return this.settings$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current settings value (non-reactive)
|
||||
*/
|
||||
get value(): AppSettings {
|
||||
return this.settings$.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific setting value
|
||||
*/
|
||||
getSetting<K extends keyof AppSettings>(key: K): AppSettings[K] {
|
||||
return this.settings$.value[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific setting
|
||||
* Automatically persists to localStorage
|
||||
*/
|
||||
updateSetting<K extends keyof AppSettings>(
|
||||
key: K,
|
||||
value: AppSettings[K],
|
||||
): void {
|
||||
const newSettings = { ...this.settings$.value, [key]: value };
|
||||
this.settings$.next(newSettings);
|
||||
saveSettings(newSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple settings at once
|
||||
* Automatically persists to localStorage
|
||||
*/
|
||||
updateSettings(updates: Partial<AppSettings>): void {
|
||||
const newSettings = { ...this.settings$.value, ...updates };
|
||||
this.settings$.next(newSettings);
|
||||
saveSettings(newSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all settings to defaults
|
||||
*/
|
||||
reset(): void {
|
||||
this.settings$.next(DEFAULT_SETTINGS);
|
||||
saveSettings(DEFAULT_SETTINGS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Global settings manager instance
|
||||
* Import this to access settings throughout the app
|
||||
*/
|
||||
export const settingsManager = new SettingsManager();
|
||||
Reference in New Issue
Block a user