mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-09 20:33:03 +02:00
Add option to hide zap bubbles on notes
This commit is contained in:
5
.changeset/many-rabbits-float.md
Normal file
5
.changeset/many-rabbits-float.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add option to hide zap bubbles on notes
|
96
src/classes/local-settings/entry.ts
Normal file
96
src/classes/local-settings/entry.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { PersistentSubject } from "../subject";
|
||||||
|
|
||||||
|
export class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
|
||||||
|
key: string;
|
||||||
|
decode?: (raw: string | null) => T | null;
|
||||||
|
encode?: (value: T) => string | null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
key: string,
|
||||||
|
initValue: T | null = null,
|
||||||
|
decode?: (raw: string | null) => T | null,
|
||||||
|
encode?: (value: T) => string | null,
|
||||||
|
) {
|
||||||
|
let value = initValue;
|
||||||
|
if (localStorage.hasOwnProperty(key)) {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (decode) value = decode(raw);
|
||||||
|
else value = raw as T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
super(value);
|
||||||
|
this.key = key;
|
||||||
|
this.decode = decode;
|
||||||
|
this.encode = encode;
|
||||||
|
}
|
||||||
|
|
||||||
|
next(value: T | null) {
|
||||||
|
if (value === null) {
|
||||||
|
localStorage.removeItem(this.key);
|
||||||
|
|
||||||
|
super.next(value);
|
||||||
|
} else {
|
||||||
|
const encoded = this.encode ? this.encode(value) : String(value);
|
||||||
|
if (encoded !== null) localStorage.setItem(this.key, encoded);
|
||||||
|
else localStorage.removeItem(this.key);
|
||||||
|
|
||||||
|
super.next(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.next(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LocalStorageEntry<T = string> extends PersistentSubject<T> {
|
||||||
|
key: string;
|
||||||
|
fallback: T;
|
||||||
|
decode?: (raw: string) => T;
|
||||||
|
encode?: (value: T) => string | null;
|
||||||
|
|
||||||
|
setDefault = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
key: string,
|
||||||
|
fallback: T,
|
||||||
|
decode?: (raw: string) => T,
|
||||||
|
encode?: (value: T) => string | null,
|
||||||
|
setDefault = false,
|
||||||
|
) {
|
||||||
|
let value = fallback;
|
||||||
|
if (localStorage.hasOwnProperty(key)) {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (decode && raw) value = decode(raw);
|
||||||
|
else if (raw) value = raw as T;
|
||||||
|
} else if (setDefault) {
|
||||||
|
const encoded = encode ? encode(fallback) : String(fallback);
|
||||||
|
if (!encoded) throw new Error("encode can not return null when setDefault is set");
|
||||||
|
localStorage.setItem(key, encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
super(value);
|
||||||
|
|
||||||
|
this.key = key;
|
||||||
|
this.decode = decode;
|
||||||
|
this.encode = encode;
|
||||||
|
this.fallback = fallback;
|
||||||
|
this.setDefault = setDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
next(value: T) {
|
||||||
|
const encoded = this.encode ? this.encode(value) : String(value);
|
||||||
|
if (encoded !== null) localStorage.setItem(this.key, encoded);
|
||||||
|
else if (this.setDefault && encoded) localStorage.setItem(this.key, encoded);
|
||||||
|
else localStorage.removeItem(this.key);
|
||||||
|
|
||||||
|
super.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
localStorage.removeItem(this.key);
|
||||||
|
super.next(this.fallback);
|
||||||
|
}
|
||||||
|
}
|
34
src/classes/local-settings/types.ts
Normal file
34
src/classes/local-settings/types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { LocalStorageEntry, NullableLocalStorageEntry } from "./entry";
|
||||||
|
|
||||||
|
export class NumberLocalStorageEntry extends LocalStorageEntry<number> {
|
||||||
|
constructor(key: string, fallback: number) {
|
||||||
|
super(
|
||||||
|
key,
|
||||||
|
fallback,
|
||||||
|
(raw) => parseInt(raw),
|
||||||
|
(value) => String(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NullableNumberLocalStorageEntry extends NullableLocalStorageEntry<number> {
|
||||||
|
constructor(key: string, fallback: number) {
|
||||||
|
super(
|
||||||
|
key,
|
||||||
|
fallback,
|
||||||
|
(raw) => (raw !== null ? parseInt(raw) : raw),
|
||||||
|
(value) => String(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BooleanLocalStorageEntry extends LocalStorageEntry<boolean> {
|
||||||
|
constructor(key: string, fallback: boolean) {
|
||||||
|
super(
|
||||||
|
key,
|
||||||
|
fallback,
|
||||||
|
(raw) => raw === "true",
|
||||||
|
(value) => String(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -46,6 +46,7 @@ import ReplyContext from "./components/reply-context";
|
|||||||
import ZapBubbles from "./components/zap-bubbles";
|
import ZapBubbles from "./components/zap-bubbles";
|
||||||
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||||
import relayHintService from "../../../services/event-relay-hint";
|
import relayHintService from "../../../services/event-relay-hint";
|
||||||
|
import localSettings from "../../../services/local-settings";
|
||||||
|
|
||||||
export type TimelineNoteProps = Omit<CardProps, "children"> & {
|
export type TimelineNoteProps = Omit<CardProps, "children"> & {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -68,6 +69,7 @@ export function TimelineNote({
|
|||||||
}: TimelineNoteProps) {
|
}: TimelineNoteProps) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
||||||
|
const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
|
||||||
const replyForm = useDisclosure();
|
const replyForm = useDisclosure();
|
||||||
|
|
||||||
const ref = useEventIntersectionRef(event);
|
const ref = useEventIntersectionRef(event);
|
||||||
@@ -124,7 +126,7 @@ export function TimelineNote({
|
|||||||
<NoteContentWithWarning event={event} />
|
<NoteContentWithWarning event={event} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
|
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
|
||||||
<ZapBubbles event={event} w="full" />
|
{!hideZapBubbles && <ZapBubbles event={event} w="full" />}
|
||||||
{showReactionsOnNewLine && reactionButtons}
|
{showReactionsOnNewLine && reactionButtons}
|
||||||
<Flex gap="2" w="full" alignItems="center">
|
<Flex gap="2" w="full" alignItems="center">
|
||||||
<ButtonGroup size="sm" variant="ghost" isDisabled={account?.readonly ?? true}>
|
<ButtonGroup size="sm" variant="ghost" isDisabled={account?.readonly ?? true}>
|
||||||
|
@@ -17,7 +17,7 @@ export default function useCacheForm<TFieldValues extends FieldValues = FieldVal
|
|||||||
const stateRef = useRef<UseFormStateReturn<TFieldValues>>(state);
|
const stateRef = useRef<UseFormStateReturn<TFieldValues>>(state);
|
||||||
stateRef.current = state;
|
stateRef.current = state;
|
||||||
|
|
||||||
// NOTE: this watches the dirty state
|
// NOTE: this watches the state
|
||||||
state.isDirty;
|
state.isDirty;
|
||||||
state.isSubmitted;
|
state.isSubmitted;
|
||||||
|
|
||||||
|
@@ -3,121 +3,12 @@ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|||||||
|
|
||||||
import { PersistentSubject } from "../classes/subject";
|
import { PersistentSubject } from "../classes/subject";
|
||||||
import { DEFAULT_SIGNAL_RELAYS } from "../const";
|
import { DEFAULT_SIGNAL_RELAYS } from "../const";
|
||||||
|
import {
|
||||||
class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
|
BooleanLocalStorageEntry,
|
||||||
key: string;
|
NullableNumberLocalStorageEntry,
|
||||||
decode?: (raw: string | null) => T | null;
|
NumberLocalStorageEntry,
|
||||||
encode?: (value: T) => string | null;
|
} from "../classes/local-settings/types";
|
||||||
|
import { LocalStorageEntry } from "../classes/local-settings/entry";
|
||||||
constructor(
|
|
||||||
key: string,
|
|
||||||
initValue: T | null = null,
|
|
||||||
decode?: (raw: string | null) => T | null,
|
|
||||||
encode?: (value: T) => string | null,
|
|
||||||
) {
|
|
||||||
let value = initValue;
|
|
||||||
if (localStorage.hasOwnProperty(key)) {
|
|
||||||
const raw = localStorage.getItem(key);
|
|
||||||
|
|
||||||
if (decode) value = decode(raw);
|
|
||||||
else value = raw as T | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
super(value);
|
|
||||||
this.key = key;
|
|
||||||
this.decode = decode;
|
|
||||||
this.encode = encode;
|
|
||||||
}
|
|
||||||
|
|
||||||
next(value: T | null) {
|
|
||||||
if (value === null) {
|
|
||||||
localStorage.removeItem(this.key);
|
|
||||||
|
|
||||||
super.next(value);
|
|
||||||
} else {
|
|
||||||
const encoded = this.encode ? this.encode(value) : String(value);
|
|
||||||
if (encoded !== null) localStorage.setItem(this.key, encoded);
|
|
||||||
else localStorage.removeItem(this.key);
|
|
||||||
|
|
||||||
super.next(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.next(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class LocalStorageEntry<T = string> extends PersistentSubject<T> {
|
|
||||||
key: string;
|
|
||||||
fallback: T;
|
|
||||||
decode?: (raw: string) => T;
|
|
||||||
encode?: (value: T) => string | null;
|
|
||||||
|
|
||||||
setDefault = false;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
key: string,
|
|
||||||
fallback: T,
|
|
||||||
decode?: (raw: string) => T,
|
|
||||||
encode?: (value: T) => string | null,
|
|
||||||
setDefault = false,
|
|
||||||
) {
|
|
||||||
let value = fallback;
|
|
||||||
if (localStorage.hasOwnProperty(key)) {
|
|
||||||
const raw = localStorage.getItem(key);
|
|
||||||
|
|
||||||
if (decode && raw) value = decode(raw);
|
|
||||||
else if (raw) value = raw as T;
|
|
||||||
} else if (setDefault) {
|
|
||||||
const encoded = encode ? encode(fallback) : String(fallback);
|
|
||||||
if (!encoded) throw new Error("encode can not return null when setDefault is set");
|
|
||||||
localStorage.setItem(key, encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
super(value);
|
|
||||||
|
|
||||||
this.key = key;
|
|
||||||
this.decode = decode;
|
|
||||||
this.encode = encode;
|
|
||||||
this.fallback = fallback;
|
|
||||||
this.setDefault = setDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
next(value: T) {
|
|
||||||
const encoded = this.encode ? this.encode(value) : String(value);
|
|
||||||
if (encoded !== null) localStorage.setItem(this.key, encoded);
|
|
||||||
else if (this.setDefault && encoded) localStorage.setItem(this.key, encoded);
|
|
||||||
else localStorage.removeItem(this.key);
|
|
||||||
|
|
||||||
super.next(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
localStorage.removeItem(this.key);
|
|
||||||
super.next(this.fallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NumberLocalStorageEntry extends LocalStorageEntry<number> {
|
|
||||||
constructor(key: string, fallback: number) {
|
|
||||||
super(
|
|
||||||
key,
|
|
||||||
fallback,
|
|
||||||
(raw) => parseInt(raw),
|
|
||||||
(value) => String(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class NullableNumberLocalStorageEntry extends NullableLocalStorageEntry<number> {
|
|
||||||
constructor(key: string, fallback: number) {
|
|
||||||
super(
|
|
||||||
key,
|
|
||||||
fallback,
|
|
||||||
(raw) => (raw !== null ? parseInt(raw) : raw),
|
|
||||||
(value) => String(value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// local relay
|
// local relay
|
||||||
const idbMaxEvents = new NumberLocalStorageEntry("nostr-idb-max-events", 10_000);
|
const idbMaxEvents = new NumberLocalStorageEntry("nostr-idb-max-events", 10_000);
|
||||||
@@ -131,6 +22,8 @@ const enableNoteThreadDrawer = new LocalStorageEntry(
|
|||||||
(v) => String(v),
|
(v) => String(v),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hideZapBubbles = new BooleanLocalStorageEntry("hide-zap-bubbles", false);
|
||||||
|
|
||||||
// webrtc relay
|
// webrtc relay
|
||||||
const webRtcLocalIdentity = new LocalStorageEntry(
|
const webRtcLocalIdentity = new LocalStorageEntry(
|
||||||
"nostr-webrtc-identity",
|
"nostr-webrtc-identity",
|
||||||
@@ -156,6 +49,7 @@ const localSettings = {
|
|||||||
idbMaxEvents,
|
idbMaxEvents,
|
||||||
wasmPersistForDays,
|
wasmPersistForDays,
|
||||||
enableNoteThreadDrawer,
|
enableNoteThreadDrawer,
|
||||||
|
hideZapBubbles,
|
||||||
webRtcLocalIdentity,
|
webRtcLocalIdentity,
|
||||||
webRtcSignalingRelays,
|
webRtcSignalingRelays,
|
||||||
webRtcRecentConnections,
|
webRtcRecentConnections,
|
||||||
|
@@ -72,7 +72,7 @@ function CommunitiesExplorePage() {
|
|||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<PeopleListSelection hideGlobalOption />
|
<PeopleListSelection hideGlobalOption />
|
||||||
<Switch onChange={showMore.onToggle} checked={showMore.isOpen}>
|
<Switch onChange={showMore.onToggle} isChecked={showMore.isOpen}>
|
||||||
Show More
|
Show More
|
||||||
</Switch>
|
</Switch>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@@ -25,6 +25,7 @@ import localSettings from "../../services/local-settings";
|
|||||||
export default function DisplaySettings() {
|
export default function DisplaySettings() {
|
||||||
const { register } = useFormContext<AppSettings>();
|
const { register } = useFormContext<AppSettings>();
|
||||||
|
|
||||||
|
const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
|
||||||
const enableNoteDrawer = useSubject(localSettings.enableNoteThreadDrawer);
|
const enableNoteDrawer = useSubject(localSettings.enableNoteThreadDrawer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -123,6 +124,21 @@ export default function DisplaySettings() {
|
|||||||
<span>Enabled: Removes all emojis in other users usernames and display names</span>
|
<span>Enabled: Removes all emojis in other users usernames and display names</span>
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<FormLabel htmlFor="hideZapBubbles" mb="0">
|
||||||
|
Hide individual zaps on notes
|
||||||
|
</FormLabel>
|
||||||
|
<Switch
|
||||||
|
id="hideZapBubbles"
|
||||||
|
isChecked={hideZapBubbles}
|
||||||
|
onChange={() => localSettings.hideZapBubbles.next(!localSettings.hideZapBubbles.value)}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<FormHelperText>
|
||||||
|
<span>Enabled: Hides individual zaps on notes in the timeline</span>
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Flex alignItems="center">
|
<Flex alignItems="center">
|
||||||
<FormLabel htmlFor="show-content-warning" mb="0">
|
<FormLabel htmlFor="show-content-warning" mb="0">
|
||||||
@@ -141,7 +157,7 @@ export default function DisplaySettings() {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Switch
|
<Switch
|
||||||
id="enableNoteDrawer"
|
id="enableNoteDrawer"
|
||||||
checked={enableNoteDrawer}
|
isChecked={enableNoteDrawer}
|
||||||
onChange={() => localSettings.enableNoteThreadDrawer.next(!localSettings.enableNoteThreadDrawer.value)}
|
onChange={() => localSettings.enableNoteThreadDrawer.next(!localSettings.enableNoteThreadDrawer.value)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@@ -60,7 +60,7 @@ function StreamsPage() {
|
|||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Flex gap="2" wrap="wrap" alignItems="center">
|
<Flex gap="2" wrap="wrap" alignItems="center">
|
||||||
<PeopleListSelection />
|
<PeopleListSelection />
|
||||||
<Switch checked={showEnded.isOpen} onChange={showEnded.onToggle}>
|
<Switch isChecked={showEnded.isOpen} onChange={showEnded.onToggle}>
|
||||||
Show Ended
|
Show Ended
|
||||||
</Switch>
|
</Switch>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@@ -48,7 +48,7 @@ export default function EventRow({ event }: { event: NostrEvent }) {
|
|||||||
{raw.isOpen && (
|
{raw.isOpen && (
|
||||||
<CopyIconButton value={stringify(event, { space: " " })} aria-label="Copy json" size="sm" />
|
<CopyIconButton value={stringify(event, { space: " " })} aria-label="Copy json" size="sm" />
|
||||||
)}
|
)}
|
||||||
<Switch size="sm" checked={!raw.isOpen} onChange={raw.onToggle}>
|
<Switch size="sm" isChecked={!raw.isOpen} onChange={raw.onToggle}>
|
||||||
Raw
|
Raw
|
||||||
</Switch>
|
</Switch>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@@ -151,7 +151,7 @@ export default function EventConsoleView() {
|
|||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<BackButton size="sm" />
|
<BackButton size="sm" />
|
||||||
<Heading size="md">Event Console</Heading>
|
<Heading size="md">Event Console</Heading>
|
||||||
<Switch size="sm" checked={queryRelay.isOpen} onChange={queryRelay.onToggle}>
|
<Switch size="sm" isChecked={queryRelay.isOpen} onChange={queryRelay.onToggle}>
|
||||||
Query Relay
|
Query Relay
|
||||||
</Switch>
|
</Switch>
|
||||||
{queryRelay.isOpen && (
|
{queryRelay.isOpen && (
|
||||||
|
@@ -117,7 +117,7 @@ export default function EventPublisherView() {
|
|||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<BackButton size="sm" />
|
<BackButton size="sm" />
|
||||||
<Heading size="md">Event Publisher</Heading>
|
<Heading size="md">Event Publisher</Heading>
|
||||||
<Switch size="sm" checked={customRelay.isOpen} onChange={customRelay.onToggle}>
|
<Switch size="sm" isChecked={customRelay.isOpen} onChange={customRelay.onToggle}>
|
||||||
Publish to Relay
|
Publish to Relay
|
||||||
</Switch>
|
</Switch>
|
||||||
{customRelay.isOpen && (
|
{customRelay.isOpen && (
|
||||||
|
Reference in New Issue
Block a user