mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-21 05:53:25 +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 useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||
import relayHintService from "../../../services/event-relay-hint";
|
||||
import localSettings from "../../../services/local-settings";
|
||||
|
||||
export type TimelineNoteProps = Omit<CardProps, "children"> & {
|
||||
event: NostrEvent;
|
||||
@@ -68,6 +69,7 @@ export function TimelineNote({
|
||||
}: TimelineNoteProps) {
|
||||
const account = useCurrentAccount();
|
||||
const { showReactions, showSignatureVerification } = useSubject(appSettings);
|
||||
const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
|
||||
const replyForm = useDisclosure();
|
||||
|
||||
const ref = useEventIntersectionRef(event);
|
||||
@@ -124,7 +126,7 @@ export function TimelineNote({
|
||||
<NoteContentWithWarning event={event} />
|
||||
</CardBody>
|
||||
<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}
|
||||
<Flex gap="2" w="full" alignItems="center">
|
||||
<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);
|
||||
stateRef.current = state;
|
||||
|
||||
// NOTE: this watches the dirty state
|
||||
// NOTE: this watches the state
|
||||
state.isDirty;
|
||||
state.isSubmitted;
|
||||
|
||||
|
@@ -3,121 +3,12 @@ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
||||
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import { DEFAULT_SIGNAL_RELAYS } from "../const";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
import {
|
||||
BooleanLocalStorageEntry,
|
||||
NullableNumberLocalStorageEntry,
|
||||
NumberLocalStorageEntry,
|
||||
} from "../classes/local-settings/types";
|
||||
import { LocalStorageEntry } from "../classes/local-settings/entry";
|
||||
|
||||
// local relay
|
||||
const idbMaxEvents = new NumberLocalStorageEntry("nostr-idb-max-events", 10_000);
|
||||
@@ -131,6 +22,8 @@ const enableNoteThreadDrawer = new LocalStorageEntry(
|
||||
(v) => String(v),
|
||||
);
|
||||
|
||||
const hideZapBubbles = new BooleanLocalStorageEntry("hide-zap-bubbles", false);
|
||||
|
||||
// webrtc relay
|
||||
const webRtcLocalIdentity = new LocalStorageEntry(
|
||||
"nostr-webrtc-identity",
|
||||
@@ -156,6 +49,7 @@ const localSettings = {
|
||||
idbMaxEvents,
|
||||
wasmPersistForDays,
|
||||
enableNoteThreadDrawer,
|
||||
hideZapBubbles,
|
||||
webRtcLocalIdentity,
|
||||
webRtcSignalingRelays,
|
||||
webRtcRecentConnections,
|
||||
|
@@ -72,7 +72,7 @@ function CommunitiesExplorePage() {
|
||||
Back
|
||||
</Button>
|
||||
<PeopleListSelection hideGlobalOption />
|
||||
<Switch onChange={showMore.onToggle} checked={showMore.isOpen}>
|
||||
<Switch onChange={showMore.onToggle} isChecked={showMore.isOpen}>
|
||||
Show More
|
||||
</Switch>
|
||||
</Flex>
|
||||
|
@@ -25,6 +25,7 @@ import localSettings from "../../services/local-settings";
|
||||
export default function DisplaySettings() {
|
||||
const { register } = useFormContext<AppSettings>();
|
||||
|
||||
const hideZapBubbles = useSubject(localSettings.hideZapBubbles);
|
||||
const enableNoteDrawer = useSubject(localSettings.enableNoteThreadDrawer);
|
||||
|
||||
return (
|
||||
@@ -123,6 +124,21 @@ export default function DisplaySettings() {
|
||||
<span>Enabled: Removes all emojis in other users usernames and display names</span>
|
||||
</FormHelperText>
|
||||
</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>
|
||||
<Flex alignItems="center">
|
||||
<FormLabel htmlFor="show-content-warning" mb="0">
|
||||
@@ -141,7 +157,7 @@ export default function DisplaySettings() {
|
||||
</FormLabel>
|
||||
<Switch
|
||||
id="enableNoteDrawer"
|
||||
checked={enableNoteDrawer}
|
||||
isChecked={enableNoteDrawer}
|
||||
onChange={() => localSettings.enableNoteThreadDrawer.next(!localSettings.enableNoteThreadDrawer.value)}
|
||||
/>
|
||||
</Flex>
|
||||
|
@@ -60,7 +60,7 @@ function StreamsPage() {
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2" wrap="wrap" alignItems="center">
|
||||
<PeopleListSelection />
|
||||
<Switch checked={showEnded.isOpen} onChange={showEnded.onToggle}>
|
||||
<Switch isChecked={showEnded.isOpen} onChange={showEnded.onToggle}>
|
||||
Show Ended
|
||||
</Switch>
|
||||
</Flex>
|
||||
|
@@ -48,7 +48,7 @@ export default function EventRow({ event }: { event: NostrEvent }) {
|
||||
{raw.isOpen && (
|
||||
<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
|
||||
</Switch>
|
||||
</Flex>
|
||||
|
@@ -151,7 +151,7 @@ export default function EventConsoleView() {
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<BackButton size="sm" />
|
||||
<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
|
||||
</Switch>
|
||||
{queryRelay.isOpen && (
|
||||
|
@@ -117,7 +117,7 @@ export default function EventPublisherView() {
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<BackButton size="sm" />
|
||||
<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
|
||||
</Switch>
|
||||
{customRelay.isOpen && (
|
||||
|
Reference in New Issue
Block a user