mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-28 20:43:33 +02:00
hide muted users in stream chat
This commit is contained in:
5
.changeset/wise-wolves-hang.md
Normal file
5
.changeset/wise-wolves-hang.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Hide muted users in stream chat
|
16
src/hooks/use-parsed-streams.ts
Normal file
16
src/hooks/use-parsed-streams.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
import { ParsedStream, parseStreamEvent } from "../helpers/nostr/stream";
|
||||||
|
|
||||||
|
export default function useParsedStreams(events: NostrEvent[]) {
|
||||||
|
return useMemo(() => {
|
||||||
|
const parsedStreams: ParsedStream[] = [];
|
||||||
|
for (const event of events) {
|
||||||
|
try {
|
||||||
|
const parsed = parseStreamEvent(event);
|
||||||
|
parsedStreams.push(parsed);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return parsedStreams.sort((a, b) => (b.starts ?? 0) - (a.starts ?? 0));
|
||||||
|
}, [events]);
|
||||||
|
}
|
15
src/hooks/use-user-mute-list.ts
Normal file
15
src/hooks/use-user-mute-list.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { useReadRelayUrls } from "./use-client-relays";
|
||||||
|
import useSubject from "./use-subject";
|
||||||
|
import userMuteListService from "../services/user-mute-list";
|
||||||
|
|
||||||
|
export default function useUserMuteList(pubkey?: string, additionalRelays?: string[], alwaysRequest = false) {
|
||||||
|
const relays = useReadRelayUrls(additionalRelays);
|
||||||
|
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
if (!pubkey) return;
|
||||||
|
return userMuteListService.requestMuteList(relays, pubkey, alwaysRequest);
|
||||||
|
}, [pubkey]);
|
||||||
|
|
||||||
|
return useSubject(sub);
|
||||||
|
}
|
14
src/services/user-mute-list.ts
Normal file
14
src/services/user-mute-list.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import replaceableEventLoaderService from "./replaceable-event-requester";
|
||||||
|
|
||||||
|
class UserMuteListService {
|
||||||
|
getMuteList(pubkey: string) {
|
||||||
|
return replaceableEventLoaderService.getEvent(10000, pubkey);
|
||||||
|
}
|
||||||
|
requestMuteList(relays: string[], pubkey: string, alwaysRequest = false) {
|
||||||
|
return replaceableEventLoaderService.requestEvent(relays, 10000, pubkey, undefined, alwaysRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMuteListService = new UserMuteListService();
|
||||||
|
|
||||||
|
export default userMuteListService;
|
@@ -13,6 +13,7 @@ import useRelaysChanged from "../../hooks/use-relays-changed";
|
|||||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||||
import PeopleListProvider, { usePeopleListContext } from "../../components/people-list-selection/people-list-provider";
|
import PeopleListProvider, { usePeopleListContext } from "../../components/people-list-selection/people-list-provider";
|
||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
|
import useParsedStreams from "../../hooks/use-parsed-streams";
|
||||||
|
|
||||||
function StreamsPage() {
|
function StreamsPage() {
|
||||||
const relays = useRelaySelectionRelays();
|
const relays = useRelaySelectionRelays();
|
||||||
@@ -44,16 +45,7 @@ function StreamsPage() {
|
|||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline.timeline);
|
||||||
const streams = useMemo(() => {
|
const streams = useParsedStreams(events);
|
||||||
const parsedStreams: ParsedStream[] = [];
|
|
||||||
for (const event of events) {
|
|
||||||
try {
|
|
||||||
const parsed = parseStreamEvent(event);
|
|
||||||
parsedStreams.push(parsed);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
return parsedStreams.sort((a, b) => (b.starts ?? 0) - (a.starts ?? 0));
|
|
||||||
}, [events]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useScroll } from "react-use";
|
import { useScroll } from "react-use";
|
||||||
import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
|
import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useParams, Navigate, useSearchParams } from "react-router-dom";
|
import { useParams, Navigate, useSearchParams, useNavigate } from "react-router-dom";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { Global, css } from "@emotion/react";
|
import { Global, css } from "@emotion/react";
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollState = useScroll(scrollBox);
|
const scrollState = useScroll(scrollBox);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const renderActions = () => {
|
const renderActions = () => {
|
||||||
const toggleButton =
|
const toggleButton =
|
||||||
@@ -110,9 +111,7 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
<StreamDebugButton stream={stream} variant="ghost" />
|
<StreamDebugButton stream={stream} variant="ghost" />
|
||||||
<RelaySelectionButton />
|
<RelaySelectionButton />
|
||||||
<Button as={RouterLink} to="/streams">
|
<Button onClick={() => navigate(-1)}>Back</Button>
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<StreamSummaryContent stream={stream} px={isMobile ? "2" : 0} />
|
<StreamSummaryContent stream={stream} px={isMobile ? "2" : 0} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useRef } from "react";
|
import { useCallback, useMemo, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -15,8 +15,6 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, buildChatMessage, getATag } from "../../../../helpers/nostr/stream";
|
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, buildChatMessage, getATag } from "../../../../helpers/nostr/stream";
|
||||||
import { useAdditionalRelayContext } from "../../../../providers/additional-relay-context";
|
|
||||||
import { useReadRelayUrls } from "../../../../hooks/use-client-relays";
|
|
||||||
import { useUserRelays } from "../../../../hooks/use-user-relays";
|
import { useUserRelays } from "../../../../hooks/use-user-relays";
|
||||||
import { RelayMode } from "../../../../classes/relay";
|
import { RelayMode } from "../../../../classes/relay";
|
||||||
import ZapModal from "../../../../components/zap-modal";
|
import ZapModal from "../../../../components/zap-modal";
|
||||||
@@ -40,6 +38,9 @@ import TopZappers from "./top-zappers";
|
|||||||
import { parseZapEvent } from "../../../../helpers/zaps";
|
import { parseZapEvent } from "../../../../helpers/zaps";
|
||||||
import { Kind } from "nostr-tools";
|
import { Kind } from "nostr-tools";
|
||||||
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
||||||
|
import useUserMuteList from "../../../../hooks/use-user-mute-list";
|
||||||
|
import { NostrEvent, isPTag } from "../../../../types/nostr-event";
|
||||||
|
import { useCurrentAccount } from "../../../../hooks/use-current-account";
|
||||||
|
|
||||||
const hideScrollbar = css`
|
const hideScrollbar = css`
|
||||||
scrollbar-width: 0;
|
scrollbar-width: 0;
|
||||||
@@ -58,6 +59,7 @@ export default function StreamChat({
|
|||||||
...props
|
...props
|
||||||
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
|
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const account = useCurrentAccount();
|
||||||
const streamRelays = useRelaySelectionRelays();
|
const streamRelays = useRelaySelectionRelays();
|
||||||
const hostReadRelays = useUserRelays(stream.host)
|
const hostReadRelays = useUserRelays(stream.host)
|
||||||
.filter((r) => r.mode & RelayMode.READ)
|
.filter((r) => r.mode & RelayMode.READ)
|
||||||
@@ -65,10 +67,23 @@ export default function StreamChat({
|
|||||||
|
|
||||||
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);
|
const relays = useMemo(() => unique([...streamRelays, ...hostReadRelays]), [hostReadRelays, streamRelays]);
|
||||||
|
|
||||||
const timeline = useTimelineLoader(`${truncatedId(stream.identifier)}-chat`, streamRelays, {
|
const hostMuteList = useUserMuteList(stream.host);
|
||||||
"#a": [getATag(stream)],
|
const muteList = useUserMuteList(account?.pubkey);
|
||||||
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
const mutedPubkeys = useMemo(
|
||||||
});
|
() => [...(hostMuteList?.tags ?? []), ...(muteList?.tags ?? [])].filter(isPTag).map((t) => t[1] as string),
|
||||||
|
[hostMuteList, muteList]
|
||||||
|
);
|
||||||
|
const eventFilter = useCallback((event: NostrEvent) => !mutedPubkeys.includes(event.pubkey), [mutedPubkeys]);
|
||||||
|
|
||||||
|
const timeline = useTimelineLoader(
|
||||||
|
`${truncatedId(stream.identifier)}-chat`,
|
||||||
|
streamRelays,
|
||||||
|
{
|
||||||
|
"#a": [getATag(stream)],
|
||||||
|
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
||||||
|
},
|
||||||
|
{ eventFilter }
|
||||||
|
);
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline).sort((a, b) => b.created_at - a.created_at);
|
const events = useSubject(timeline.timeline).sort((a, b) => b.created_at - a.created_at);
|
||||||
|
|
||||||
|
@@ -1,13 +1,15 @@
|
|||||||
import { Flex } from "@chakra-ui/react";
|
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { truncatedId } from "../../helpers/nostr/event";
|
import { truncatedId } from "../../helpers/nostr/event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
import GenericNoteTimeline from "../../components/timeline-page/generic-note-timeline";
|
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
import useParsedStreams from "../../hooks/use-parsed-streams";
|
||||||
|
import StreamCard from "../streams/components/stream-card";
|
||||||
|
|
||||||
export default function UserStreamsTab() {
|
export default function UserStreamsTab() {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
@@ -23,12 +25,19 @@ export default function UserStreamsTab() {
|
|||||||
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
|
const events = useSubject(timeline.timeline);
|
||||||
|
const streams = useParsedStreams(events);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider<string> callback={callback}>
|
<Flex p="2" gap="2" overflow="hidden" direction="column">
|
||||||
<Flex direction="column" gap="2" pt="4" pb="8">
|
<IntersectionObserverProvider<string> callback={callback}>
|
||||||
<GenericNoteTimeline timeline={timeline} />
|
<SimpleGrid minChildWidth="20rem" spacing="2">
|
||||||
|
{streams.map((stream) => (
|
||||||
|
<StreamCard key={stream.event.id} stream={stream} />
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
<TimelineActionAndStatus timeline={timeline} />
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
</Flex>
|
</IntersectionObserverProvider>
|
||||||
</IntersectionObserverProvider>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user