attempt to auto unmute

This commit is contained in:
hzrd149
2023-09-30 10:41:56 -05:00
parent d10cb2b34a
commit e23aa9f69f
8 changed files with 191 additions and 52 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
/>
);
}

View File

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