mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
rebuild stream moderation view layout
This commit is contained in:
parent
8e44b7ab04
commit
19dc44c968
@ -44,6 +44,7 @@
|
||||
"react-force-graph-2d": "^1.25.1",
|
||||
"react-force-graph-3d": "^1.23.1",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-mosaic-component": "^6.1.0",
|
||||
"react-photo-album": "^2.3.0",
|
||||
"react-qr-barcode-scanner": "^1.0.6",
|
||||
"react-router-dom": "^6.15.0",
|
||||
|
@ -58,7 +58,6 @@ import DrawerSubViewProvider from "./providers/drawer-sub-view-provider";
|
||||
import CommunitiesHomeView from "./views/communities";
|
||||
import CommunityFindByNameView from "./views/community/find-by-name";
|
||||
import CommunityView from "./views/community/index";
|
||||
import StreamModerationView from "./views/tools/stream-moderation";
|
||||
import PopularRelaysView from "./views/relays/popular";
|
||||
|
||||
const NetworkView = React.lazy(() => import("./views/tools/network"));
|
||||
@ -67,6 +66,7 @@ const StreamsView = React.lazy(() => import("./views/streams"));
|
||||
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||
const SearchView = React.lazy(() => import("./views/search"));
|
||||
const MapView = React.lazy(() => import("./views/map"));
|
||||
const StreamModerationView = React.lazy(() => import("./views/tools/stream-moderation"));
|
||||
|
||||
const overrideReactTextareaAutocompleteStyles = css`
|
||||
.rta__autocomplete {
|
||||
@ -127,6 +127,7 @@ const router = createHashRouter([
|
||||
</PageProviders>
|
||||
),
|
||||
},
|
||||
{ path: "tools/stream-moderation", element: <StreamModerationView /> },
|
||||
{
|
||||
path: "map",
|
||||
element: <MapView />,
|
||||
@ -181,7 +182,6 @@ const router = createHashRouter([
|
||||
{ path: "", element: <ToolsHomeView /> },
|
||||
{ path: "network", element: <NetworkView /> },
|
||||
{ path: "network-graph", element: <NetworkGraphView /> },
|
||||
{ path: "stream-moderation", element: <StreamModerationView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Badge, Flex, FlexProps } from "@chakra-ui/react";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
import Hls from "hls.js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
@ -13,14 +13,14 @@ export function LiveVideoPlayer({
|
||||
autoPlay,
|
||||
poster,
|
||||
...props
|
||||
}: FlexProps & { stream?: string; autoPlay?: boolean; poster?: string }) {
|
||||
}: Omit<BoxProps, "children"> & { stream?: string; autoPlay?: boolean; poster?: string }) {
|
||||
const video = useRef<HTMLVideoElement>(null);
|
||||
const [status, setStatus] = useState<VideoStatus>();
|
||||
|
||||
useEffect(() => {
|
||||
if (stream && video.current && !video.current.src && Hls.isSupported()) {
|
||||
try {
|
||||
const hls = new Hls();
|
||||
const hls = new Hls({ capLevelToPlayerSize: true });
|
||||
hls.loadSource(stream);
|
||||
hls.attachMedia(video.current);
|
||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
@ -43,15 +43,15 @@ export function LiveVideoPlayer({
|
||||
}, [video, stream]);
|
||||
|
||||
return (
|
||||
<Flex justifyContent="center" alignItems="center" {...props} position="relative">
|
||||
<video
|
||||
ref={video}
|
||||
playsInline={true}
|
||||
controls={status === VideoStatus.Online}
|
||||
autoPlay={autoPlay}
|
||||
poster={poster}
|
||||
style={{ maxHeight: "100%", maxWidth: "100%", width: "100%" }}
|
||||
/>
|
||||
</Flex>
|
||||
<Box
|
||||
as="video"
|
||||
ref={video}
|
||||
playsInline={true}
|
||||
controls={status === VideoStatus.Online}
|
||||
autoPlay={autoPlay}
|
||||
poster={poster}
|
||||
style={{ maxHeight: "100%", maxWidth: "100%", width: "100%" }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import { MagicInput, RefType } from "../../../../components/magic-textarea";
|
||||
import StreamZapButton from "../../components/stream-zap-button";
|
||||
import { nostrBuildUploadImage } from "../../../../helpers/nostr-build";
|
||||
|
||||
export default function ChatMessageForm({ stream }: { stream: ParsedStream }) {
|
||||
export default function ChatMessageForm({ stream, hideZapButton }: { stream: ParsedStream; hideZapButton?: boolean }) {
|
||||
const toast = useToast();
|
||||
const emojis = useContextEmojis();
|
||||
const streamRelays = useRelaySelectionRelays();
|
||||
@ -85,7 +85,7 @@ export default function ChatMessageForm({ stream }: { stream: ParsedStream }) {
|
||||
Send
|
||||
</Button>
|
||||
</Flex>
|
||||
<StreamZapButton stream={stream} onZap={reset} initComment={getValues().content} />
|
||||
{!hideZapButton && <StreamZapButton stream={stream} onZap={reset} initComment={getValues().content} />}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
@ -1,188 +0,0 @@
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Select,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import useParsedStreams from "../../hooks/use-parsed-streams";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { ParsedStream, STREAM_KIND, getATag } from "../../helpers/nostr/stream";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { getEventUID } from "../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useStreamChatTimeline from "../streams/stream/stream-chat/use-stream-chat-timeline";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import StreamChat from "../streams/stream/stream-chat";
|
||||
import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
|
||||
import { useMuteModalContext } from "../../providers/mute-modal-provider";
|
||||
import RelaySelectionProvider from "../../providers/relay-selection-provider";
|
||||
import useUserMuteList from "../../hooks/use-user-mute-list";
|
||||
import { isPubkeyInList } from "../../helpers/nostr/lists";
|
||||
import ZapMessageMemo from "../streams/stream/stream-chat/zap-message";
|
||||
|
||||
function UserCard({ pubkey }: { pubkey: string }) {
|
||||
const { isMuted, mute, unmute, expiration } = useUserMuteFunctions(pubkey);
|
||||
const { openModal } = useMuteModalContext();
|
||||
|
||||
let buttons: ReactNode | null = null;
|
||||
if (isMuted) {
|
||||
if (expiration === Infinity) {
|
||||
buttons = <Button onClick={unmute}>Unban</Button>;
|
||||
} else {
|
||||
buttons = <Button onClick={unmute}>Unmute ({dayjs.unix(expiration).fromNow()})</Button>;
|
||||
}
|
||||
} else {
|
||||
buttons = (
|
||||
<>
|
||||
<Button onClick={() => openModal(pubkey)}>Mute</Button>
|
||||
<Button onClick={mute}>Ban</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap="2" direction="row" alignItems="center">
|
||||
{!isMuted && <UserAvatar pubkey={pubkey} noProxy size="sm" />}
|
||||
<UserLink pubkey={pubkey} />
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
{buttons}
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function UserMuteCard({ stream, ...props }: Omit<CardProps, "children"> & { stream: ParsedStream }) {
|
||||
const account = useCurrentAccount()!;
|
||||
const streamChatTimeline = useStreamChatTimeline(stream);
|
||||
|
||||
// refresh when a new event
|
||||
useSubject(streamChatTimeline.events.onEvent);
|
||||
const chatEvents = streamChatTimeline.events.getSortedEvents();
|
||||
|
||||
const muteList = useUserMuteList(account.pubkey);
|
||||
const pubkeysInChat = useMemo(() => {
|
||||
const pubkeys: string[] = [];
|
||||
for (const event of chatEvents) {
|
||||
if (!pubkeys.includes(event.pubkey)) pubkeys.push(event.pubkey);
|
||||
}
|
||||
return pubkeys;
|
||||
}, [chatEvents]);
|
||||
|
||||
const peopleInChat = pubkeysInChat.filter((pubkey) => !isPubkeyInList(muteList, pubkey));
|
||||
const mutedPubkeys = pubkeysInChat.filter((pubkey) => isPubkeyInList(muteList, pubkey));
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader pt="2" px="2" pb="0">
|
||||
<Heading size="md">Users in chat</Heading>
|
||||
</CardHeader>
|
||||
<CardBody p="2" gap="2" display="flex" overflowY="auto" overflowX="hidden" flexDirection="column">
|
||||
{peopleInChat.map((pubkey) => (
|
||||
<UserCard key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
{mutedPubkeys.length > 0 && (
|
||||
<>
|
||||
<Heading size="sm">Muted</Heading>
|
||||
<Divider />
|
||||
{mutedPubkeys.map((pubkey) => (
|
||||
<UserCard key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ZapMessagesCard({ stream, ...props }: Omit<CardProps, "children"> & { 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);
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader pt="2" px="2" pb="0">
|
||||
<Heading size="md">Zap messages</Heading>
|
||||
</CardHeader>
|
||||
<CardBody p="2" gap="2" display="flex" overflowY="auto" overflowX="hidden" flexDirection="column">
|
||||
{zapMessages.map((event) => (
|
||||
<ZapMessageMemo key={event.id} zap={event} stream={stream} />
|
||||
))}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamModerationDashboard({ stream }: { stream: ParsedStream }) {
|
||||
return (
|
||||
<Flex gap="2" overflow="hidden" height="100%">
|
||||
<UserMuteCard stream={stream} flex={1} />
|
||||
<ZapMessagesCard stream={stream} flex={1} />
|
||||
<StreamChat stream={stream} flex={1} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamModerationPage() {
|
||||
const account = useCurrentAccount()!;
|
||||
const readRelays = useReadRelayUrls();
|
||||
|
||||
const timeline = useTimelineLoader(account.pubkey + "-streams", readRelays, [
|
||||
{
|
||||
authors: [account.pubkey],
|
||||
kinds: [STREAM_KIND],
|
||||
},
|
||||
{ "#p": [account.pubkey], kinds: [STREAM_KIND] },
|
||||
]);
|
||||
|
||||
const streamEvents = useSubject(timeline.timeline);
|
||||
const streams = useParsedStreams(streamEvents);
|
||||
|
||||
const [selected, setSelected] = useState<ParsedStream>();
|
||||
|
||||
return (
|
||||
<Flex direction="column" p="2" overflow="hidden" gap="2" h="100vh">
|
||||
<Flex gap="2" flexShrink={0}>
|
||||
<Select
|
||||
placeholder="Select stream"
|
||||
value={selected && getATag(selected)}
|
||||
onChange={(e) => setSelected(streams.find((s) => getATag(s) === e.target.value))}
|
||||
>
|
||||
{streams.map((stream) => (
|
||||
<option key={getEventUID(stream.event)} value={getATag(stream)}>
|
||||
{stream.title} ({stream.status})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
{selected && (
|
||||
<RelaySelectionProvider additionalDefaults={selected.relays ?? []}>
|
||||
<StreamModerationDashboard stream={selected} />
|
||||
</RelaySelectionProvider>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StreamModerationView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<StreamModerationPage />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
27
src/views/tools/stream-moderation/chat-card.tsx
Normal file
27
src/views/tools/stream-moderation/chat-card.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { forwardRef, 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";
|
||||
|
||||
const ChatCard = forwardRef<HTMLDivElement, DashboardCardProps>(({ stream, children, ...props }, ref) => {
|
||||
const timeline = useStreamChatTimeline(stream);
|
||||
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<Flex flex={1} direction="column" overflow="hidden" p={0}>
|
||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||
<StreamChatLog ref={scrollBox} stream={stream} flex={1} px="4" py="2" mb="2" />
|
||||
<ChatMessageForm stream={stream} hideZapButton />
|
||||
</IntersectionObserverProvider>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
export default memo(ChatCard);
|
4
src/views/tools/stream-moderation/common.tsx
Normal file
4
src/views/tools/stream-moderation/common.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
import { CardHeader, CardHeaderProps, CardProps, Heading } from "@chakra-ui/react";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
export type DashboardCardProps = CardProps & { stream: ParsedStream };
|
121
src/views/tools/stream-moderation/index.tsx
Normal file
121
src/views/tools/stream-moderation/index.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Flex, Select } from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Mosaic, MosaicNode, MosaicWindow } from "react-mosaic-component";
|
||||
import "./styles.css";
|
||||
import "react-mosaic-component/react-mosaic-component.css";
|
||||
|
||||
import useParsedStreams from "../../../hooks/use-parsed-streams";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import { ParsedStream, STREAM_KIND, getATag } from "../../../helpers/nostr/stream";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import RequireCurrentAccount from "../../../providers/require-current-account";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import { ArrowLeftSIcon } from "../../../components/icons";
|
||||
import RelaySelectionProvider from "../../../providers/relay-selection-provider";
|
||||
import UsersCard from "./users-card";
|
||||
import ZapsCard from "./zaps-card";
|
||||
import ChatCard from "./chat-card";
|
||||
import VideoCard from "./video-card";
|
||||
|
||||
const defaultLayout: MosaicNode<string> = {
|
||||
direction: "row",
|
||||
first: {
|
||||
direction: "column",
|
||||
first: "video",
|
||||
second: {
|
||||
direction: "row",
|
||||
first: "users",
|
||||
second: "zaps",
|
||||
},
|
||||
},
|
||||
second: "chat",
|
||||
splitPercentage: 60,
|
||||
};
|
||||
|
||||
function StreamModerationDashboard({ stream }: { stream: ParsedStream }) {
|
||||
const [value, setValue] = useState<MosaicNode<string> | null>(defaultLayout);
|
||||
|
||||
const ELEMENT_MAP: Record<string, JSX.Element> = {
|
||||
video: <VideoCard stream={stream} />,
|
||||
chat: <ChatCard stream={stream} />,
|
||||
users: <UsersCard stream={stream} />,
|
||||
zaps: <ZapsCard stream={stream} />,
|
||||
};
|
||||
const TITLE_MAP: Record<string, string> = {
|
||||
video: "Stream",
|
||||
chat: "Stream Chat",
|
||||
users: "Users in chat",
|
||||
zaps: "Zaps",
|
||||
};
|
||||
|
||||
return (
|
||||
<Mosaic<string>
|
||||
className="chakra-theme"
|
||||
renderTile={(id, path) => (
|
||||
<MosaicWindow<string> path={path} title={TITLE_MAP[id]}>
|
||||
{ELEMENT_MAP[id]}
|
||||
</MosaicWindow>
|
||||
)}
|
||||
value={value}
|
||||
onChange={(v) => setValue(v)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamModerationPage() {
|
||||
const navigate = useNavigate();
|
||||
const account = useCurrentAccount()!;
|
||||
const readRelays = useReadRelayUrls();
|
||||
|
||||
const timeline = useTimelineLoader(account.pubkey + "-streams", readRelays, [
|
||||
{
|
||||
authors: [account.pubkey],
|
||||
kinds: [STREAM_KIND],
|
||||
},
|
||||
{ "#p": [account.pubkey], kinds: [STREAM_KIND] },
|
||||
]);
|
||||
|
||||
const streamEvents = useSubject(timeline.timeline);
|
||||
const streams = useParsedStreams(streamEvents);
|
||||
|
||||
const [selected, setSelected] = useState<ParsedStream>();
|
||||
|
||||
return (
|
||||
<Flex direction="column" w="full" h="full">
|
||||
<Flex gap="2" p="2" pb="0">
|
||||
<Button onClick={() => navigate(-1)} leftIcon={<ArrowLeftSIcon />}>
|
||||
Back
|
||||
</Button>
|
||||
<Select
|
||||
placeholder="Select stream"
|
||||
value={selected && getATag(selected)}
|
||||
onChange={(e) => setSelected(streams.find((s) => getATag(s) === e.target.value))}
|
||||
w="lg"
|
||||
>
|
||||
{streams.map((stream) => (
|
||||
<option key={getEventUID(stream.event)} value={getATag(stream)}>
|
||||
{stream.title} ({stream.status})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
{selected && (
|
||||
<RelaySelectionProvider additionalDefaults={selected.relays ?? []}>
|
||||
<StreamModerationDashboard stream={selected} />
|
||||
</RelaySelectionProvider>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StreamModerationView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<StreamModerationPage />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
26
src/views/tools/stream-moderation/styles.css
Normal file
26
src/views/tools/stream-moderation/styles.css
Normal file
@ -0,0 +1,26 @@
|
||||
.chakra-theme .mosaic {
|
||||
background: none;
|
||||
}
|
||||
.chakra-theme .mosaic-window {
|
||||
border: 1px solid var(--chakra-colors-chakra-border-color);
|
||||
border-radius: var(--chakra-sizes-1);
|
||||
}
|
||||
.chakra-theme .mosaic-window .mosaic-window-toolbar {
|
||||
height: auto;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
}
|
||||
.chakra-theme .mosaic-window .mosaic-window-title {
|
||||
font-size: var(--chakra-fontSizes-lg);
|
||||
color: var(--chakra-colors-chakra-body-text);
|
||||
padding: var(--chakra-sizes-2);
|
||||
background: var(--chakra-colors-chakra-body-bg);
|
||||
}
|
||||
.chakra-theme .mosaic-window .mosaic-window-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--chakra-colors-chakra-body-bg);
|
||||
}
|
||||
.chakra-theme .mosaic-window .mosaic-window-controls {
|
||||
display: none;
|
||||
}
|
85
src/views/tools/stream-moderation/users-card.tsx
Normal file
85
src/views/tools/stream-moderation/users-card.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { ReactNode, memo, useMemo } from "react";
|
||||
import { Button, ButtonGroup, Divider, Flex, Heading } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useStreamChatTimeline from "../../streams/stream/stream-chat/use-stream-chat-timeline";
|
||||
import { UserAvatar } from "../../../components/user-avatar";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
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";
|
||||
|
||||
const UserCard = ({ pubkey }: { pubkey: string }) => {
|
||||
const { isMuted, mute, unmute, expiration } = useUserMuteFunctions(pubkey);
|
||||
const { openModal } = useMuteModalContext();
|
||||
|
||||
let buttons: ReactNode | null = null;
|
||||
if (isMuted) {
|
||||
if (expiration === Infinity) {
|
||||
buttons = <Button onClick={unmute}>Unban</Button>;
|
||||
} else {
|
||||
buttons = <Button onClick={unmute}>Unmute ({dayjs.unix(expiration).fromNow()})</Button>;
|
||||
}
|
||||
} else {
|
||||
buttons = (
|
||||
<>
|
||||
<Button onClick={() => openModal(pubkey)}>Mute</Button>
|
||||
<Button onClick={mute}>Ban</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap="2" direction="row" alignItems="center">
|
||||
{!isMuted && <UserAvatar pubkey={pubkey} noProxy size="sm" />}
|
||||
<UserLink pubkey={pubkey} />
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
{buttons}
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
function UsersCard({ stream, ...props }: DashboardCardProps) {
|
||||
const account = useCurrentAccount()!;
|
||||
const streamChatTimeline = useStreamChatTimeline(stream);
|
||||
|
||||
// refresh when a new event
|
||||
useSubject(streamChatTimeline.events.onEvent);
|
||||
const chatEvents = streamChatTimeline.events.getSortedEvents();
|
||||
|
||||
const muteList = useUserMuteList(account.pubkey);
|
||||
const pubkeysInChat = useMemo(() => {
|
||||
const pubkeys: string[] = [];
|
||||
for (const event of chatEvents) {
|
||||
if (!pubkeys.includes(event.pubkey)) pubkeys.push(event.pubkey);
|
||||
}
|
||||
return pubkeys;
|
||||
}, [chatEvents]);
|
||||
|
||||
const peopleInChat = pubkeysInChat.filter((pubkey) => !isPubkeyInList(muteList, pubkey));
|
||||
const mutedPubkeys = pubkeysInChat.filter((pubkey) => isPubkeyInList(muteList, pubkey));
|
||||
|
||||
return (
|
||||
<Flex flex={1} p="2" gap="2" display="flex" overflowY="auto" overflowX="hidden" flexDirection="column">
|
||||
{peopleInChat.map((pubkey) => (
|
||||
<UserCard key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
{mutedPubkeys.length > 0 && (
|
||||
<>
|
||||
<Heading size="sm">Muted</Heading>
|
||||
<Divider />
|
||||
{mutedPubkeys.map((pubkey) => (
|
||||
<UserCard key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(UsersCard);
|
12
src/views/tools/stream-moderation/video-card.tsx
Normal file
12
src/views/tools/stream-moderation/video-card.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { memo } from "react";
|
||||
|
||||
import { DashboardCardProps } from "./common";
|
||||
import { LiveVideoPlayer } from "../../../components/live-video-player";
|
||||
|
||||
function LiveVideoCard({ stream, children, ...props }: DashboardCardProps) {
|
||||
return (
|
||||
<LiveVideoPlayer stream={stream.streaming || stream.recording} autoPlay={false} poster={stream.image} maxH="50vh" />
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LiveVideoCard);
|
26
src/views/tools/stream-moderation/zaps-card.tsx
Normal file
26
src/views/tools/stream-moderation/zaps-card.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { memo } from "react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
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";
|
||||
|
||||
function ZapsCard({ stream, ...props }: DashboardCardProps) {
|
||||
const streamChatTimeline = useStreamChatTimeline(stream);
|
||||
|
||||
// refresh when a new event
|
||||
useSubject(streamChatTimeline.events.onEvent);
|
||||
const zapMessages = streamChatTimeline.events.getSortedEvents().filter((event) => event.kind === Kind.Zap);
|
||||
|
||||
return (
|
||||
<Flex flex={1} p="2" gap="2" overflowY="auto" overflowX="hidden" flexDirection="column">
|
||||
{zapMessages.map((event) => (
|
||||
<ZapMessageMemo key={event.id} zap={event} stream={stream} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ZapsCard);
|
126
yarn.lock
126
yarn.lock
@ -934,6 +934,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.9.2":
|
||||
version "7.23.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d"
|
||||
integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.22.15", "@babel/template@^7.22.5":
|
||||
version "7.22.15"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
|
||||
@ -2394,6 +2401,21 @@
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
||||
|
||||
"@react-dnd/asap@^5.0.1":
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
|
||||
integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==
|
||||
|
||||
"@react-dnd/invariant@^4.0.1":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df"
|
||||
integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==
|
||||
|
||||
"@react-dnd/shallowequal@^4.0.1":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4"
|
||||
integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
|
||||
|
||||
"@remix-run/router@1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6"
|
||||
@ -3200,6 +3222,11 @@ ci-info@^3.1.0, ci-info@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
|
||||
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
|
||||
|
||||
classnames@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||
|
||||
clean-stack@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
||||
@ -3758,6 +3785,20 @@ dir-glob@^3.0.1:
|
||||
dependencies:
|
||||
path-type "^4.0.0"
|
||||
|
||||
dnd-core@^16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19"
|
||||
integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==
|
||||
dependencies:
|
||||
"@react-dnd/asap" "^5.0.1"
|
||||
"@react-dnd/invariant" "^4.0.1"
|
||||
redux "^4.2.0"
|
||||
|
||||
dnd-multi-backend@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dnd-multi-backend/-/dnd-multi-backend-8.0.3.tgz#2cc8121ad2b6e6164e3044be9ffdfe994ab6bdb0"
|
||||
integrity sha512-yFFARotr+OEJk787Fsj+V52pi6j7+Pt/CRp3IR2Ai3fnxA/z6J54T7+gxkXzXu4cvxTNE7NiBzzAaJ2f7JjFTw==
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.16"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||
@ -4486,7 +4527,7 @@ hls.js@^1.4.10:
|
||||
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.4.12.tgz#2022daa29d10c662387d80a5297f8330f8ef5ee2"
|
||||
integrity sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA==
|
||||
|
||||
hoist-non-react-statics@^3.3.1:
|
||||
hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||
@ -4559,6 +4600,11 @@ ignore@^5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
|
||||
immutability-helper@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/immutability-helper/-/immutability-helper-3.1.1.tgz#2b86b2286ed3b1241c9e23b7b21e0444f52f77b7"
|
||||
integrity sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==
|
||||
|
||||
import-fresh@^3.2.1:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||
@ -5699,7 +5745,7 @@ process@^0.11.10:
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
||||
|
||||
prop-types@15, prop-types@^15.6.2:
|
||||
prop-types@15, prop-types@^15.6.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@ -5765,6 +5811,15 @@ randombytes@^2.1.0:
|
||||
dependencies:
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
rdndmb-html5-to-touch@^8.0.0:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/rdndmb-html5-to-touch/-/rdndmb-html5-to-touch-8.0.3.tgz#dca0dd429520650a298f961a75dedd63d59808ad"
|
||||
integrity sha512-VfIbLjlL9NAnZzc2M5fGPCNkDyK12+ahgILGO5RjS7jkgUlxwB0c/XvxVQNfY/2ocg7isTY/G7tqxJk5fSTZAA==
|
||||
dependencies:
|
||||
dnd-multi-backend "^8.0.3"
|
||||
react-dnd-html5-backend "^16.0.1"
|
||||
react-dnd-touch-backend "^16.0.1"
|
||||
|
||||
react-clientside-effect@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
|
||||
@ -5772,6 +5827,45 @@ react-clientside-effect@^1.2.6:
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.13"
|
||||
|
||||
react-dnd-html5-backend@^16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6"
|
||||
integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==
|
||||
dependencies:
|
||||
dnd-core "^16.0.1"
|
||||
|
||||
react-dnd-multi-backend@^8.0.0:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd-multi-backend/-/react-dnd-multi-backend-8.0.3.tgz#4587645539d28d9985e4c39e9d45ddaffc671e87"
|
||||
integrity sha512-IwH7Mf6R05KIFohX0hHMTluoAvuUD8SO15KCD+9fY0nJ4nc1FGCMCSyMZw8R1XNStKp+JnNg3ZMtiaf5DebSUg==
|
||||
dependencies:
|
||||
dnd-multi-backend "^8.0.3"
|
||||
react-dnd-preview "^8.0.3"
|
||||
|
||||
react-dnd-preview@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd-preview/-/react-dnd-preview-8.0.3.tgz#71f22ab64b43ddc7ed8a39bb9b03523ec0dba9e4"
|
||||
integrity sha512-s69Ro47QYDthDhj73iQ0VioMCjtlZ1AytKBDkQaHKm5DTjA8D2bIaFKCBQd330QEW0SIzqLJrZGCSlIY2xraJg==
|
||||
|
||||
react-dnd-touch-backend@^16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz#e73f8169e2b9fac0f687970f875cac0a4d02d6e2"
|
||||
integrity sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==
|
||||
dependencies:
|
||||
"@react-dnd/invariant" "^4.0.1"
|
||||
dnd-core "^16.0.1"
|
||||
|
||||
react-dnd@^16.0.1:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37"
|
||||
integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==
|
||||
dependencies:
|
||||
"@react-dnd/invariant" "^4.0.1"
|
||||
"@react-dnd/shallowequal" "^4.0.1"
|
||||
dnd-core "^16.0.1"
|
||||
fast-deep-equal "^3.1.3"
|
||||
hoist-non-react-statics "^3.3.2"
|
||||
|
||||
react-dom@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
@ -5845,6 +5939,22 @@ react-kapsule@2:
|
||||
fromentries "^1.3.2"
|
||||
jerrypick "^1.1.1"
|
||||
|
||||
react-mosaic-component@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-mosaic-component/-/react-mosaic-component-6.1.0.tgz#67383085680e8604d0fcb6d61387be19665da308"
|
||||
integrity sha512-iWrNUSdW6HK9SB6kaj7/auvIGZWlyEFR8ulQKC9lskY047uluo5ur4fiuZTNroUTZvGqL02AiLzBBj1+et8RZA==
|
||||
dependencies:
|
||||
classnames "^2.3.2"
|
||||
immutability-helper "^3.1.1"
|
||||
lodash "^4.17.21"
|
||||
prop-types "^15.8.1"
|
||||
rdndmb-html5-to-touch "^8.0.0"
|
||||
react-dnd "^16.0.1"
|
||||
react-dnd-html5-backend "^16.0.1"
|
||||
react-dnd-multi-backend "^8.0.0"
|
||||
react-dnd-touch-backend "^16.0.1"
|
||||
uuid "^9.0.0"
|
||||
|
||||
react-photo-album@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-photo-album/-/react-photo-album-2.3.0.tgz#262afa60691d8ed5e25b8c8a73cec339ec515652"
|
||||
@ -5991,6 +6101,13 @@ redent@^3.0.0:
|
||||
indent-string "^4.0.0"
|
||||
strip-indent "^3.0.0"
|
||||
|
||||
redux@^4.2.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
|
||||
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
|
||||
regenerate-unicode-properties@^10.1.0:
|
||||
version "10.1.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480"
|
||||
@ -6964,6 +7081,11 @@ uuid@^8.3.2:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuid@^9.0.0:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
|
||||
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||
|
Loading…
x
Reference in New Issue
Block a user