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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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