support k 10000 mute lists

This commit is contained in:
hzrd149 2023-09-13 09:25:41 -05:00
parent d254eaca16
commit e04aa5c02c
23 changed files with 243 additions and 39 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Hide muted users in stream views

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to add user to k 10000 mute list

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Filter out muted users in home feed

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Hide muted users in threads

View File

@ -391,3 +391,33 @@ export const DrawerIcon = createIcon({
d: "M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM8 5H4V19H8V5ZM10 5V19H20V5H10Z",
defaultProps,
});
export const UnmuteIcon = createIcon({
displayName: "UnmuteIcon",
d: "M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z",
defaultProps,
});
export const MuteIcon = createIcon({
displayName: "MuteIcon",
d: "M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z",
defaultProps,
});
export const AppearanceIcon = createIcon({
displayName: "AppearanceIcon",
d: "M12 2C17.5222 2 22 5.97778 22 10.8889C22 13.9556 19.5111 16.4444 16.4444 16.4444H14.4778C13.5556 16.4444 12.8111 17.1889 12.8111 18.1111C12.8111 18.5333 12.9778 18.9222 13.2333 19.2111C13.5 19.5111 13.6667 19.9 13.6667 20.3333C13.6667 21.2556 12.9 22 12 22C6.47778 22 2 17.5222 2 12C2 6.47778 6.47778 2 12 2ZM10.8111 18.1111C10.8111 16.0843 12.451 14.4444 14.4778 14.4444H16.4444C18.4065 14.4444 20 12.851 20 10.8889C20 7.1392 16.4677 4 12 4C7.58235 4 4 7.58235 4 12C4 16.19 7.2226 19.6285 11.324 19.9718C10.9948 19.4168 10.8111 18.7761 10.8111 18.1111ZM7.5 12C6.67157 12 6 11.3284 6 10.5C6 9.67157 6.67157 9 7.5 9C8.32843 9 9 9.67157 9 10.5C9 11.3284 8.32843 12 7.5 12ZM16.5 12C15.6716 12 15 11.3284 15 10.5C15 9.67157 15.6716 9 16.5 9C17.3284 9 18 9.67157 18 10.5C18 11.3284 17.3284 12 16.5 12ZM12 9C11.1716 9 10.5 8.32843 10.5 7.5C10.5 6.67157 11.1716 6 12 6C12.8284 6 13.5 6.67157 13.5 7.5C13.5 8.32843 12.8284 9 12 9Z",
defaultProps,
});
export const DatabaseIcon = createIcon({
displayName: "DatabaseIcon",
d: "M5 12.5C5 12.8134 5.46101 13.3584 6.53047 13.8931C7.91405 14.5849 9.87677 15 12 15C14.1232 15 16.0859 14.5849 17.4695 13.8931C18.539 13.3584 19 12.8134 19 12.5V10.3287C17.35 11.3482 14.8273 12 12 12C9.17273 12 6.64996 11.3482 5 10.3287V12.5ZM19 15.3287C17.35 16.3482 14.8273 17 12 17C9.17273 17 6.64996 16.3482 5 15.3287V17.5C5 17.8134 5.46101 18.3584 6.53047 18.8931C7.91405 19.5849 9.87677 20 12 20C14.1232 20 16.0859 19.5849 17.4695 18.8931C18.539 18.3584 19 17.8134 19 17.5V15.3287ZM3 17.5V7.5C3 5.01472 7.02944 3 12 3C16.9706 3 21 5.01472 21 7.5V17.5C21 19.9853 16.9706 22 12 22C7.02944 22 3 19.9853 3 17.5ZM12 10C14.1232 10 16.0859 9.58492 17.4695 8.89313C18.539 8.3584 19 7.81342 19 7.5C19 7.18658 18.539 6.6416 17.4695 6.10687C16.0859 5.41508 14.1232 5 12 5C9.87677 5 7.91405 5.41508 6.53047 6.10687C5.46101 6.6416 5 7.18658 5 7.5C5 7.81342 5.46101 8.3584 6.53047 8.89313C7.91405 9.58492 9.87677 10 12 10Z",
defaultProps,
});
export const PerformanceIcon = createIcon({
displayName: "PerformanceIcon",
d: "M20 13C20 15.2091 19.1046 17.2091 17.6569 18.6569L19.0711 20.0711C20.8807 18.2614 22 15.7614 22 13 22 7.47715 17.5228 3 12 3 6.47715 3 2 7.47715 2 13 2 15.7614 3.11929 18.2614 4.92893 20.0711L6.34315 18.6569C4.89543 17.2091 4 15.2091 4 13 4 8.58172 7.58172 5 12 5 16.4183 5 20 8.58172 20 13ZM15.293 8.29297 10.793 12.793 12.2072 14.2072 16.7072 9.70718 15.293 8.29297Z",
defaultProps,
});

View File

@ -7,7 +7,17 @@ import { getSharableEventAddress } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
import { ClipboardIcon, CodeIcon, ExternalLinkIcon, LikeIcon, RelayIcon, RepostIcon, TrashIcon } from "../icons";
import {
ClipboardIcon,
CodeIcon,
ExternalLinkIcon,
LikeIcon,
MuteIcon,
RelayIcon,
RepostIcon,
TrashIcon,
UnmuteIcon,
} from "../icons";
import NoteReactionsModal from "./note-zaps-modal";
import NoteDebugModal from "../debug-modals/note-debug-modal";
import { useCurrentAccount } from "../../hooks/use-current-account";
@ -16,11 +26,13 @@ import { useDeleteEventContext } from "../../providers/delete-event-provider";
import clientRelaysService from "../../services/client-relays";
import { handleEventFromRelay } from "../../services/event-relays";
import NostrPublishAction from "../../classes/nostr-publish-action";
import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) => {
const account = useCurrentAccount();
const infoModal = useDisclosure();
const reactionsModal = useDisclosure();
const { isMuted, mute, unmute } = useUserMuteFunctions(event.pubkey);
const { deleteEvent } = useDeleteEventContext();
@ -29,13 +41,9 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
const broadcast = useCallback(() => {
const missingRelays = clientRelaysService.getWriteUrls();
const pub = new NostrPublishAction("Broadcast", missingRelays, event, 5000);
pub.onResult.subscribe((result) => {
if (result.status) {
handleEventFromRelay(result.relay, event);
}
if (result.status) handleEventFromRelay(result.relay, event);
});
}, []);
@ -44,14 +52,16 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
return (
<>
<MenuIconButton {...props}>
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Zaps/Reactions
</MenuItem>
{address && (
<MenuItem onClick={() => window.open(buildAppSelectUrl(address), "_blank")} icon={<ExternalLinkIcon />}>
View in app...
</MenuItem>
)}
{account?.pubkey !== event.pubkey && (
<MenuItem onClick={isMuted ? unmute : mute} icon={isMuted ? <UnmuteIcon /> : <MuteIcon />} color="red.500">
{isMuted ? "Unmute User" : "Mute User"}
</MenuItem>
)}
<MenuItem onClick={() => copyToClipboard("nostr:" + address)} icon={<RepostIcon />}>
Copy Share Link
</MenuItem>
@ -71,6 +81,9 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
View Raw
</MenuItem>
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Zaps/Reactions
</MenuItem>
</MenuIconButton>
{infoModal.isOpen && (

View File

@ -14,11 +14,12 @@ import {
} from "@chakra-ui/react";
import { useCurrentAccount } from "../hooks/use-current-account";
import { ArrowDownSIcon, FollowIcon, PlusCircleIcon, UnfollowIcon } from "./icons";
import { ArrowDownSIcon, FollowIcon, MuteIcon, PlusCircleIcon, UnfollowIcon, UnmuteIcon } from "./icons";
import useUserLists from "../hooks/use-user-lists";
import {
PEOPLE_LIST_KIND,
createEmptyContactList,
createEmptyMuteList,
draftAddPerson,
draftRemovePerson,
getListName,
@ -33,6 +34,8 @@ import useUserContactList from "../hooks/use-user-contact-list";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import useAsyncErrorHandler from "../hooks/use-async-error-handler";
import NewListModal from "../views/lists/components/new-list-modal";
import useUserMuteList from "../hooks/use-user-mute-list";
import useUserMuteFunctions from "../hooks/use-user-mute-functions";
function UsersLists({ pubkey }: { pubkey: string }) {
const toast = useToast();
@ -116,6 +119,7 @@ export const UserFollowButton = ({ pubkey, showLists, ...props }: UserFollowButt
const account = useCurrentAccount()!;
const { requestSignature } = useSigningContext();
const contacts = useUserContactList(account?.pubkey, [], true);
const { isMuted, mute, unmute } = useUserMuteFunctions(pubkey);
const isFollowing = isPubkeyInList(contacts, pubkey);
const isDisabled = account?.readonly ?? true;
@ -149,6 +153,16 @@ export const UserFollowButton = ({ pubkey, showLists, ...props }: UserFollowButt
Follow
</MenuItem>
)}
{account?.pubkey !== pubkey && (
<MenuItem
onClick={isMuted ? unmute : mute}
icon={isMuted ? <UnmuteIcon /> : <MuteIcon />}
color="red.500"
isDisabled={isDisabled}
>
{isMuted ? "Unmute" : "Mute"}
</MenuItem>
)}
{account && (
<>
<MenuDivider />

View File

@ -26,6 +26,10 @@ export type ParsedStream = {
relays?: string[];
};
export function getStreamHost(stream: NostrEvent) {
return stream.tags.filter(isPTag)[0]?.[1] ?? stream.pubkey;
}
export function parseStreamEvent(stream: NostrEvent): ParsedStream {
const title = stream.tags.find((t) => t[0] === "title")?.[1];
const summary = stream.tags.find((t) => t[0] === "summary")?.[1];
@ -59,12 +63,11 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
status = "ended";
}
const host = stream.tags.filter(isPTag)[0]?.[1] ?? stream.pubkey;
const tags = unique(stream.tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1] as string));
return {
author: stream.pubkey,
host,
host: getStreamHost(stream),
event: stream,
updated: stream.created_at,
streaming,

View File

@ -0,0 +1,23 @@
import { useCallback, useMemo } from "react";
import { useCurrentAccount } from "./use-current-account";
import useUserMuteList from "./use-user-mute-list";
import { getPubkeysFromList } from "../helpers/nostr/lists";
import { NostrEvent } from "../types/nostr-event";
import { STREAM_KIND, getStreamHost } from "../helpers/nostr/stream";
export default function useUserMuteFilter(pubkey?: string) {
const account = useCurrentAccount();
const muteList = useUserMuteList(pubkey || account?.pubkey);
const pubkeys = useMemo(() => (muteList ? getPubkeysFromList(muteList).map((p) => p.pubkey) : []), [muteList]);
return useCallback(
(event: NostrEvent) => {
if (event.kind === STREAM_KIND) {
if (pubkeys.includes(getStreamHost(event))) return true;
}
return pubkeys.includes(event.pubkey);
},
[pubkeys],
);
}

View File

@ -0,0 +1,31 @@
import NostrPublishAction from "../classes/nostr-publish-action";
import { createEmptyMuteList, draftAddPerson, draftRemovePerson, isPubkeyInList } from "../helpers/nostr/lists";
import { useSigningContext } from "../providers/signing-provider";
import clientRelaysService from "../services/client-relays";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import useAsyncErrorHandler from "./use-async-error-handler";
import { useCurrentAccount } from "./use-current-account";
import useUserMuteList from "./use-user-mute-list";
export default function useUserMuteFunctions(pubkey: string) {
const account = useCurrentAccount()!;
const { requestSignature } = useSigningContext();
const muteList = useUserMuteList(account?.pubkey, [], true);
const isMuted = isPubkeyInList(muteList, pubkey);
const mute = useAsyncErrorHandler(async () => {
const draft = draftAddPerson(muteList || createEmptyMuteList(), pubkey);
const signed = await requestSignature(draft);
new NostrPublishAction("Mute", clientRelaysService.getWriteUrls(), signed);
replaceableEventLoaderService.handleEvent(signed);
});
const unmute = useAsyncErrorHandler(async () => {
const draft = draftRemovePerson(muteList || createEmptyMuteList(), pubkey);
const signed = await requestSignature(draft);
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
replaceableEventLoaderService.handleEvent(signed);
});
return { isMuted, mute, unmute };
}

View File

@ -1,7 +1,8 @@
import { useMemo } from "react";
import useReplaceableEvent from "./use-replaceable-event";
import { PEOPLE_LIST_KIND, getPubkeysFromList } from "../helpers/nostr/lists";
import useUserMuteList from "./use-user-mute-list";
import { useMemo } from "react";
export default function useUserMuteLists(pubkey?: string, additionalRelays: string[] = [], alwaysRequest = true) {
const muteList = useUserMuteList(pubkey, additionalRelays, alwaysRequest);

View File

@ -1,10 +1,12 @@
import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from "react";
import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo } from "react";
import { Kind } from "nostr-tools";
import { useReadRelayUrls } from "../hooks/use-client-relays";
import { useCurrentAccount } from "../hooks/use-current-account";
import { TimelineLoader } from "../classes/timeline-loader";
import timelineCacheService from "../services/timeline-cache";
import useUserMuteFilter from "../hooks/use-user-mute-filter";
import { NostrEvent } from "../types/nostr-event";
type NotificationTimelineContextType = {
timeline?: TimelineLoader;
@ -29,6 +31,18 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil
: undefined;
}, [account?.pubkey]);
const userMuteFilter = useUserMuteFilter(account?.pubkey);
const eventFilter = useCallback(
(event: NostrEvent) => {
if (userMuteFilter(event)) return false;
return true;
},
[userMuteFilter],
);
useEffect(() => {
timeline?.setFilter(eventFilter);
}, [timeline, eventFilter]);
useEffect(() => {
if (timeline && account?.pubkey) {
timeline.setQuery([{ "#p": [account?.pubkey], kinds: [Kind.Text, Kind.Repost, Kind.Reaction, Kind.Zap] }]);

View File

@ -12,16 +12,19 @@ import RelaySelectionButton from "../../components/relay-selection/relay-selecti
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
import { NostrRequestFilter } from "../../types/nostr-query";
import useUserMuteFilter from "../../hooks/use-user-mute-filter";
function HomePage() {
const timelinePageEventFilter = useTimelinePageEventFilter();
const showReplies = useDisclosure();
const muteFilter = useUserMuteFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (muteFilter(event)) return false;
if (!showReplies.isOpen && isReply(event)) return false;
return timelinePageEventFilter(event);
},
[timelinePageEventFilter, showReplies.isOpen],
[timelinePageEventFilter, showReplies.isOpen, muteFilter],
);
const { relays } = useRelaySelectionContext();

View File

@ -9,6 +9,7 @@ import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../comp
import { getSharableEventAddress } from "../../../helpers/nip19";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
import { isSpecialListKind } from "../../../helpers/nostr/lists";
export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
const account = useCurrentAccount();
@ -33,7 +34,7 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit
</MenuItem>
</>
)}
{account?.pubkey === list.pubkey && (
{account?.pubkey === list.pubkey && !isSpecialListKind(list.kind) && (
<MenuItem icon={<TrashIcon />} color="red.500" onClick={() => deleteEvent(list)}>
Delete List
</MenuItem>

View File

@ -1,11 +1,12 @@
import { Button, ButtonGroup, Flex, useDisclosure } from "@chakra-ui/react";
import { useState } from "react";
import { Alert, AlertIcon, Button, ButtonGroup, Flex, useDisclosure } from "@chakra-ui/react";
import { ArrowDownSIcon, ArrowUpSIcon, ReplyIcon } from "../../../components/icons";
import { Note } from "../../../components/note";
import { countReplies, ThreadItem } from "../../../helpers/thread";
import { TrustProvider } from "../../../providers/trust";
import ReplyForm from "./reply-form";
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
export type ThreadItemProps = {
post: ThreadItem;
@ -18,13 +19,27 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
const toggle = () => setShowReplies((v) => !v);
const showReplyForm = useDisclosure();
const muteFilter = useUserMuteFilter();
const [alwaysShow, setAlwaysShow] = useState(false);
const numberOfReplies = countReplies(post);
const isMuted = muteFilter(post.event);
return (
<Flex direction="column" gap="2">
<TrustProvider trust={focusId === post.event.id ? true : undefined}>
<Note event={post.event} borderColor={focusId === post.event.id ? "blue.500" : undefined} hideDrawerButton />
</TrustProvider>
{isMuted && !alwaysShow ? (
<Alert status="warning">
<AlertIcon />
Muted user
<Button size="xs" ml="auto" onClick={() => setAlwaysShow(true)}>
Show anyway
</Button>
</Alert>
) : (
<TrustProvider trust={focusId === post.event.id ? true : undefined}>
<Note event={post.event} borderColor={focusId === post.event.id ? "blue.500" : undefined} hideDrawerButton />
</TrustProvider>
)}
{showReplyForm.isOpen && (
<ReplyForm item={post} onCancel={showReplyForm.onClose} onSubmitted={showReplyForm.onClose} />
)}

View File

@ -9,6 +9,7 @@ import {
ButtonGroup,
} from "@chakra-ui/react";
import { clearCacheData, deleteDatabase } from "../../services/db";
import { DatabaseIcon } from "../../components/icons";
export default function DatabaseSettings() {
const [clearing, setClearing] = useState(false);
@ -28,7 +29,8 @@ export default function DatabaseSettings() {
return (
<AccordionItem>
<h2>
<AccordionButton>
<AccordionButton fontSize="xl">
<DatabaseIcon mr="2" />
<Box as="span" flex="1" textAlign="left">
Database
</Box>

View File

@ -11,10 +11,10 @@ import {
AccordionIcon,
FormHelperText,
Input,
Stack,
Select,
} from "@chakra-ui/react";
import { AppSettings } from "../../services/settings/migrations";
import { AppearanceIcon } from "../../components/icons";
export default function DisplaySettings() {
const { register } = useFormContext<AppSettings>();
@ -22,7 +22,8 @@ export default function DisplaySettings() {
return (
<AccordionItem>
<h2>
<AccordionButton>
<AccordionButton fontSize="xl">
<AppearanceIcon mr="2" />
<Box as="span" flex="1" textAlign="left">
Display
</Box>

View File

@ -23,9 +23,10 @@ export default function LightningSettings() {
return (
<AccordionItem>
<h2>
<AccordionButton>
<AccordionButton fontSize="xl">
<LightningIcon mr="2" />
<Box as="span" flex="1" textAlign="left">
Lightning <LightningIcon color="yellow.400" />
Lightning
</Box>
<AccordionIcon />
</AccordionButton>

View File

@ -16,6 +16,7 @@ import {
} from "@chakra-ui/react";
import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
import { PerformanceIcon } from "../../components/icons";
export default function PerformanceSettings() {
const { register, formState } = useFormContext<AppSettings>();
@ -23,7 +24,8 @@ export default function PerformanceSettings() {
return (
<AccordionItem>
<h2>
<AccordionButton>
<AccordionButton fontSize="xl">
<PerformanceIcon mr="2" />
<Box as="span" flex="1" textAlign="left">
Performance
</Box>

View File

@ -17,6 +17,7 @@ import { useFormContext } from "react-hook-form";
import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
import { createCorsUrl } from "../../helpers/cors";
import { SpyIcon } from "../../components/icons";
async function validateInvidiousUrl(url?: string) {
if (!url) return true;
@ -46,7 +47,8 @@ export default function PrivacySettings() {
return (
<AccordionItem>
<h2>
<AccordionButton>
<AccordionButton fontSize="xl">
<SpyIcon mr="2" />
<Box as="span" flex="1" textAlign="left">
Privacy
</Box>

View File

@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import { Divider, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import IntersectionObserverProvider from "../../providers/intersection-observer";
@ -15,10 +15,21 @@ import TimelineActionAndStatus from "../../components/timeline-page/timeline-act
import useParsedStreams from "../../hooks/use-parsed-streams";
import { NostrRequestFilter } from "../../types/nostr-query";
import { useAppTitle } from "../../hooks/use-app-title";
import useUserMuteFilter from "../../hooks/use-user-mute-filter";
import { NostrEvent } from "../../types/nostr-event";
function StreamsPage() {
useAppTitle("Streams");
const relays = useRelaySelectionRelays();
const userMuteFilter = useUserMuteFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (userMuteFilter(event)) return false;
return true;
},
[userMuteFilter],
);
const { filter, listId } = usePeopleListContext();
const query = useMemo<NostrRequestFilter>(() => {
@ -29,7 +40,7 @@ function StreamsPage() {
];
}, [filter, listId]);
const timeline = useTimelineLoader(`${listId}-streams`, relays, query, { enabled: !!filter });
const timeline = useTimelineLoader(`${listId}-streams`, relays, query, { enabled: !!filter, eventFilter });
useRelaysChanged(relays, () => timeline.reset());

View File

@ -4,22 +4,23 @@ import { Kind } from "nostr-tools";
import { getEventUID } from "../../../../helpers/nostr/events";
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, getATag } from "../../../../helpers/nostr/stream";
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import { NostrEvent, isPTag } from "../../../../types/nostr-event";
import useUserMuteList from "../../../../hooks/use-user-mute-list";
import { NostrEvent } from "../../../../types/nostr-event";
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
import { useCurrentAccount } from "../../../../hooks/use-current-account";
import useStreamGoal from "../../../../hooks/use-stream-goal";
import { NostrQuery } from "../../../../types/nostr-query";
import useUserMuteFilter from "../../../../hooks/use-user-mute-filter";
export default function useStreamChatTimeline(stream: ParsedStream) {
const account = useCurrentAccount();
const streamRelays = useRelaySelectionRelays();
const hostMuteList = useUserMuteList(stream.host);
const muteList = useUserMuteList(account?.pubkey);
const mutedPubkeys = useMemo(
() => [...(hostMuteList?.tags ?? []), ...(muteList?.tags ?? [])].filter(isPTag).map((t) => t[1] as string),
[hostMuteList, muteList],
const hostMuteFilter = useUserMuteFilter(stream.host);
const userMuteFilter = useUserMuteFilter(account?.pubkey);
const eventFilter = useCallback(
(event: NostrEvent) => !(hostMuteFilter(event) || userMuteFilter(event)),
[hostMuteFilter, userMuteFilter],
);
const goal = useStreamGoal(stream);
@ -38,7 +39,5 @@ export default function useStreamChatTimeline(stream: ParsedStream) {
}
return streamQuery;
}, [stream, goal]);
const eventFilter = useCallback((event: NostrEvent) => !mutedPubkeys.includes(event.pubkey), [mutedPubkeys]);
return useTimelineLoader(`${getEventUID(stream.event)}-chat`, streamRelays, query, { eventFilter });
}

View File

@ -4,7 +4,16 @@ import { nip19 } from "nostr-tools";
import { useCopyToClipboard } from "react-use";
import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
import { ChatIcon, ClipboardIcon, CodeIcon, ExternalLinkIcon, RelayIcon, SpyIcon } from "../../../components/icons";
import {
ChatIcon,
ClipboardIcon,
CodeIcon,
ExternalLinkIcon,
MuteIcon,
RelayIcon,
SpyIcon,
UnmuteIcon,
} from "../../../components/icons";
import accountService from "../../../services/account";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../../helpers/user-metadata";
@ -14,16 +23,20 @@ import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
import { truncatedId } from "../../../helpers/nostr/events";
import useUserMuteFunctions from "../../../hooks/use-user-mute-functions";
import { useCurrentAccount } from "../../../hooks/use-current-account";
export const UserProfileMenu = ({
pubkey,
showRelaySelectionModal,
...props
}: { pubkey: string; showRelaySelectionModal?: () => void } & Omit<MenuIconButtonProps, "children">) => {
const account = useCurrentAccount();
const metadata = useUserMetadata(pubkey);
const userRelays = useUserRelays(pubkey);
const infoModal = useDisclosure();
const sharableId = useSharableProfileId(pubkey);
const { isMuted, mute, unmute } = useUserMuteFunctions(pubkey);
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
@ -45,6 +58,11 @@ export const UserProfileMenu = ({
<MenuItem onClick={() => window.open(buildAppSelectUrl(sharableId), "_blank")} icon={<ExternalLinkIcon />}>
View in app...
</MenuItem>
{account?.pubkey !== pubkey && (
<MenuItem onClick={isMuted ? unmute : mute} icon={isMuted ? <UnmuteIcon /> : <MuteIcon />} color="red.500">
{isMuted ? "Unmute User" : "Mute User"}
</MenuItem>
)}
<MenuItem icon={<ChatIcon fontSize="1.5em" />} as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}`}>
Direct messages
</MenuItem>