Add option to hide zap bubbles on notes

This commit is contained in:
hzrd149
2024-08-27 09:25:52 +03:00
parent 808ca81036
commit 9deb032845
12 changed files with 170 additions and 123 deletions

View File

@@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to hide zap bubbles on notes

View 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);
}
}

View 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),
);
}
}

View File

@@ -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}>

View File

@@ -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;

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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 && (