mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-08-03 07:02:12 +02:00
attempt to auto unmute
This commit is contained in:
@@ -127,7 +127,14 @@ const router = createHashRouter([
|
||||
</PageProviders>
|
||||
),
|
||||
},
|
||||
{ path: "tools/stream-moderation", element: <StreamModerationView /> },
|
||||
{
|
||||
path: "tools/stream-moderation",
|
||||
element: (
|
||||
<PageProviders>
|
||||
<StreamModerationView />
|
||||
</PageProviders>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "map",
|
||||
element: <MapView />,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { HTMLProps, useEffect, useRef, useState } from "react";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
import Hls from "hls.js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export enum VideoStatus {
|
||||
Online = "online",
|
||||
@@ -12,8 +12,14 @@ export function LiveVideoPlayer({
|
||||
stream,
|
||||
autoPlay,
|
||||
poster,
|
||||
muted,
|
||||
...props
|
||||
}: Omit<BoxProps, "children"> & { stream?: string; autoPlay?: boolean; poster?: string }) {
|
||||
}: Omit<BoxProps, "children"> & {
|
||||
stream?: string;
|
||||
autoPlay?: boolean;
|
||||
poster?: string;
|
||||
muted?: HTMLProps<HTMLVideoElement>["muted"];
|
||||
}) {
|
||||
const video = useRef<HTMLVideoElement>(null);
|
||||
const [status, setStatus] = useState<VideoStatus>();
|
||||
|
||||
@@ -50,6 +56,7 @@ export function LiveVideoPlayer({
|
||||
controls={status === VideoStatus.Online}
|
||||
autoPlay={autoPlay}
|
||||
poster={poster}
|
||||
muted={muted}
|
||||
style={{ maxHeight: "100%", maxWidth: "100%", width: "100%" }}
|
||||
{...props}
|
||||
/>
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
@@ -15,6 +19,7 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { useInterval } from "react-use";
|
||||
|
||||
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
@@ -23,6 +28,7 @@ import {
|
||||
createEmptyMuteList,
|
||||
getPubkeysExpiration,
|
||||
muteListAddPubkey,
|
||||
muteListRemovePubkey,
|
||||
pruneExpiredPubkeys,
|
||||
} from "../helpers/nostr/mute-list";
|
||||
import { cloneList } from "../helpers/nostr/lists";
|
||||
@@ -31,10 +37,10 @@ import NostrPublishAction from "../classes/nostr-publish-action";
|
||||
import clientRelaysService from "../services/client-relays";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import useUserMuteList from "../hooks/use-user-mute-list";
|
||||
import { useInterval } from "react-use";
|
||||
import { DraftNostrEvent } from "../types/nostr-event";
|
||||
import { UserAvatar } from "../components/user-avatar";
|
||||
import { UserLink } from "../components/user-link";
|
||||
import { ArrowDownSIcon } from "../components/icons";
|
||||
|
||||
type MuteModalContextType = {
|
||||
openModal: (pubkey: string) => void;
|
||||
@@ -119,63 +125,163 @@ function MuteModal({ pubkey, onClose, ...props }: Omit<ModalProps, "children"> &
|
||||
);
|
||||
}
|
||||
|
||||
function UnmuteModal({}) {
|
||||
function UnmuteHandler() {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount()!;
|
||||
const { requestSignature } = useSigningContext();
|
||||
const muteList = useUserMuteList(account?.pubkey, [], { ignoreCache: true });
|
||||
const modal = useDisclosure();
|
||||
|
||||
const unmuteAll = async () => {
|
||||
if (!muteList) return;
|
||||
try {
|
||||
let draft: DraftNostrEvent = cloneList(muteList);
|
||||
draft = pruneExpiredPubkeys(draft);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const check = async () => {
|
||||
if (!muteList) return;
|
||||
const now = dayjs().unix();
|
||||
const expirations = getPubkeysExpiration(muteList);
|
||||
const expired = Object.entries(expirations).filter(([pubkey, ex]) => ex < now);
|
||||
|
||||
if (expired.length > 0) {
|
||||
const accepted = await unmuteAll();
|
||||
if (!accepted) modal.onOpen();
|
||||
} else if (modal.isOpen) modal.onClose();
|
||||
};
|
||||
|
||||
useInterval(check, 10 * 1000);
|
||||
|
||||
return modal.isOpen ? <UnmuteModal onClose={modal.onClose} isOpen={modal.isOpen} /> : null;
|
||||
}
|
||||
|
||||
function UnmuteModal({ onClose }: Omit<ModalProps, "children">) {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount()!;
|
||||
const { requestSignature } = useSigningContext();
|
||||
const muteList = useUserMuteList(account?.pubkey, [], { ignoreCache: true });
|
||||
|
||||
const modal = useDisclosure();
|
||||
const removeExpiredMutes = async () => {
|
||||
const getExpiredPubkeys = useCallback(() => {
|
||||
if (!muteList) return [];
|
||||
const now = dayjs().unix();
|
||||
const expirations = getPubkeysExpiration(muteList);
|
||||
|
||||
return Object.entries(expirations).filter(([pubkey, ex]) => ex < now);
|
||||
}, [muteList]);
|
||||
|
||||
const unmuteAll = async () => {
|
||||
if (!muteList) return;
|
||||
try {
|
||||
// unmute users
|
||||
let draft: DraftNostrEvent = cloneList(muteList);
|
||||
draft = pruneExpiredPubkeys(muteList);
|
||||
draft = pruneExpiredPubkeys(draft);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Unmute Users", clientRelaysService.getWriteUrls(), signed);
|
||||
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
modal.onClose();
|
||||
onClose();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
const extendAll = async (expiration: number) => {
|
||||
if (!muteList) return;
|
||||
try {
|
||||
const expired = getExpiredPubkeys();
|
||||
let draft: DraftNostrEvent = cloneList(muteList);
|
||||
draft = pruneExpiredPubkeys(draft);
|
||||
for (const [pubkey] of expired) {
|
||||
draft = muteListAddPubkey(draft, pubkey, expiration);
|
||||
}
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Extend mute", clientRelaysService.getWriteUrls(), signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const getExpiredPubkeys = () => {
|
||||
if (!muteList) return [];
|
||||
const now = dayjs().unix();
|
||||
const expirations = getPubkeysExpiration(muteList);
|
||||
return Object.entries(expirations)
|
||||
.filter(([pubkey, ex]) => ex < now)
|
||||
.map(([pubkey]) => pubkey);
|
||||
};
|
||||
useInterval(() => {
|
||||
const unmuteUser = async (pubkey: string) => {
|
||||
if (!muteList) return;
|
||||
if (!modal.isOpen && getExpiredPubkeys().length > 0) {
|
||||
modal.onOpen();
|
||||
}
|
||||
}, 30 * 1000);
|
||||
try {
|
||||
let draft: DraftNostrEvent = cloneList(muteList);
|
||||
draft = muteListRemovePubkey(draft, pubkey);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Unmute", clientRelaysService.getWriteUrls(), signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
const extendUser = async (pubkey: string, expiration: number) => {
|
||||
if (!muteList) return;
|
||||
try {
|
||||
let draft: DraftNostrEvent = cloneList(muteList);
|
||||
draft = muteListRemovePubkey(draft, pubkey);
|
||||
draft = muteListAddPubkey(draft, pubkey, expiration);
|
||||
|
||||
const signed = await requestSignature(draft);
|
||||
new NostrPublishAction("Extend mute", clientRelaysService.getWriteUrls(), signed);
|
||||
replaceableEventLoaderService.handleEvent(signed);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const expiredPubkeys = getExpiredPubkeys().map(([pubkey]) => pubkey);
|
||||
return (
|
||||
<Modal onClose={modal.onClose} size="lg" isOpen={modal.isOpen}>
|
||||
<Modal onClose={onClose} isOpen size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader p="4">Unmute temporary muted users</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody display="flex" flexWrap="wrap" gap="2" px="4" py="0">
|
||||
{getExpiredPubkeys().map((pubkey) => (
|
||||
<ModalBody display="flex" flexDirection="column" gap="2" px="4" py="0">
|
||||
{expiredPubkeys.map((pubkey) => (
|
||||
<Flex gap="2" key={pubkey} alignItems="center">
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||
<Menu>
|
||||
<MenuButton as={Button} size="sm" ml="auto" rightIcon={<ArrowDownSIcon />}>
|
||||
Extend
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => extendUser(pubkey, Infinity)}>Forever</MenuItem>
|
||||
<MenuItem onClick={() => extendUser(pubkey, dayjs().add(30, "minutes").unix())}>30 Minutes</MenuItem>
|
||||
<MenuItem onClick={() => extendUser(pubkey, dayjs().add(1, "day").unix())}>1 Day</MenuItem>
|
||||
<MenuItem onClick={() => extendUser(pubkey, dayjs().add(1, "week").unix())}>1 Week</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Button onClick={() => unmuteUser(pubkey)} size="sm">
|
||||
Unmute
|
||||
</Button>
|
||||
</Flex>
|
||||
))}
|
||||
</ModalBody>
|
||||
<ModalFooter p="4">
|
||||
<Button onClick={modal.onClose} mr="3">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" onClick={removeExpiredMutes}>
|
||||
<Menu>
|
||||
<MenuButton as={Button} mr="2" rightIcon={<ArrowDownSIcon />}>
|
||||
Extend
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => extendAll(Infinity)}>Forever</MenuItem>
|
||||
<MenuItem onClick={() => extendAll(dayjs().add(30, "minutes").unix())}>30 Minutes</MenuItem>
|
||||
<MenuItem onClick={() => extendAll(dayjs().add(1, "day").unix())}>1 Day</MenuItem>
|
||||
<MenuItem onClick={() => extendAll(dayjs().add(1, "week").unix())}>1 Week</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Button colorScheme="brand" onClick={unmuteAll}>
|
||||
Unmute all
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
@@ -199,8 +305,8 @@ export default function MuteModalProvider({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<MuteModalContext.Provider value={context}>
|
||||
{children}
|
||||
<UnmuteModal />
|
||||
{muteUser && <MuteModal isOpen onClose={() => setMuteUser("")} pubkey={muteUser} />}
|
||||
<UnmuteHandler />
|
||||
</MuteModalContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@@ -1,14 +1,14 @@
|
||||
import { forwardRef, memo, useRef } from "react";
|
||||
import { memo, useRef } from "react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
|
||||
import { DashboardCardProps } from "./common";
|
||||
import useStreamChatTimeline from "../../streams/stream/stream-chat/use-stream-chat-timeline";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../../providers/intersection-observer";
|
||||
import StreamChatLog from "../../streams/stream/stream-chat/chat-log";
|
||||
import ChatMessageForm from "../../streams/stream/stream-chat/stream-chat-form";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
const ChatCard = forwardRef<HTMLDivElement, DashboardCardProps>(({ stream, children, ...props }, ref) => {
|
||||
function ChatCard({ stream }: { stream: ParsedStream }) {
|
||||
const timeline = useStreamChatTimeline(stream);
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
@@ -22,6 +22,6 @@ const ChatCard = forwardRef<HTMLDivElement, DashboardCardProps>(({ stream, child
|
||||
</IntersectionObserverProvider>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default memo(ChatCard);
|
||||
|
@@ -1,4 +0,0 @@
|
||||
import { CardHeader, CardHeaderProps, CardProps, Heading } from "@chakra-ui/react";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
export type DashboardCardProps = CardProps & { stream: ParsedStream };
|
@@ -1,4 +1,4 @@
|
||||
import { ReactNode, memo, useMemo } from "react";
|
||||
import { ReactNode, memo, useMemo, useState } from "react";
|
||||
import { Button, ButtonGroup, Divider, Flex, Heading } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
@@ -11,9 +11,17 @@ import useUserMuteFunctions from "../../../hooks/use-user-mute-functions";
|
||||
import { useMuteModalContext } from "../../../providers/mute-modal-provider";
|
||||
import useUserMuteList from "../../../hooks/use-user-mute-list";
|
||||
import { isPubkeyInList } from "../../../helpers/nostr/lists";
|
||||
import { DashboardCardProps } from "./common";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
import { useInterval } from "react-use";
|
||||
|
||||
const UserCard = ({ pubkey }: { pubkey: string }) => {
|
||||
function Countdown({ time }: { time: number }) {
|
||||
const [now, setNow] = useState(dayjs().unix());
|
||||
useInterval(() => setNow(dayjs().unix()), 1000);
|
||||
|
||||
return <span>{time - now + "s"}</span>;
|
||||
}
|
||||
|
||||
function UserCard({ pubkey }: { pubkey: string }) {
|
||||
const { isMuted, mute, unmute, expiration } = useUserMuteFunctions(pubkey);
|
||||
const { openModal } = useMuteModalContext();
|
||||
|
||||
@@ -22,7 +30,11 @@ const UserCard = ({ pubkey }: { pubkey: string }) => {
|
||||
if (expiration === Infinity) {
|
||||
buttons = <Button onClick={unmute}>Unban</Button>;
|
||||
} else {
|
||||
buttons = <Button onClick={unmute}>Unmute ({dayjs.unix(expiration).fromNow()})</Button>;
|
||||
buttons = (
|
||||
<Button onClick={unmute}>
|
||||
Unmute (<Countdown time={expiration} />)
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
buttons = (
|
||||
@@ -42,9 +54,9 @@ const UserCard = ({ pubkey }: { pubkey: string }) => {
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function UsersCard({ stream, ...props }: DashboardCardProps) {
|
||||
function UsersCard({ stream }: { stream: ParsedStream }) {
|
||||
const account = useCurrentAccount()!;
|
||||
const streamChatTimeline = useStreamChatTimeline(stream);
|
||||
|
||||
|
@@ -1,11 +1,17 @@
|
||||
import { memo } from "react";
|
||||
|
||||
import { DashboardCardProps } from "./common";
|
||||
import { LiveVideoPlayer } from "../../../components/live-video-player";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
function LiveVideoCard({ stream, children, ...props }: DashboardCardProps) {
|
||||
function LiveVideoCard({ stream }: { stream: ParsedStream }) {
|
||||
return (
|
||||
<LiveVideoPlayer stream={stream.streaming || stream.recording} autoPlay={false} poster={stream.image} maxH="50vh" />
|
||||
<LiveVideoPlayer
|
||||
stream={stream.streaming || stream.recording}
|
||||
autoPlay={stream.streaming ? true : undefined}
|
||||
poster={stream.image}
|
||||
maxH="50vh"
|
||||
muted
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -4,15 +4,20 @@ import { Kind } from "nostr-tools";
|
||||
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import useStreamChatTimeline from "../../streams/stream/stream-chat/use-stream-chat-timeline";
|
||||
import { DashboardCardProps } from "./common";
|
||||
import ZapMessageMemo from "../../streams/stream/stream-chat/zap-message";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
function ZapsCard({ stream, ...props }: DashboardCardProps) {
|
||||
function ZapsCard({ stream }: { stream: ParsedStream }) {
|
||||
const streamChatTimeline = useStreamChatTimeline(stream);
|
||||
|
||||
// refresh when a new event
|
||||
useSubject(streamChatTimeline.events.onEvent);
|
||||
const zapMessages = streamChatTimeline.events.getSortedEvents().filter((event) => event.kind === Kind.Zap);
|
||||
const zapMessages = streamChatTimeline.events.getSortedEvents().filter((event) => {
|
||||
if (stream.starts && event.created_at < stream.starts) return false;
|
||||
if (stream.ends && event.created_at > stream.ends) return false;
|
||||
if (event.kind !== Kind.Zap) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex flex={1} p="2" gap="2" overflowY="auto" overflowX="hidden" flexDirection="column">
|
||||
|
Reference in New Issue
Block a user