mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-26 19:47:25 +02:00
rebuild stream moderation view layout
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
"react-force-graph-2d": "^1.25.1",
|
"react-force-graph-2d": "^1.25.1",
|
||||||
"react-force-graph-3d": "^1.23.1",
|
"react-force-graph-3d": "^1.23.1",
|
||||||
"react-hook-form": "^7.45.4",
|
"react-hook-form": "^7.45.4",
|
||||||
|
"react-mosaic-component": "^6.1.0",
|
||||||
"react-photo-album": "^2.3.0",
|
"react-photo-album": "^2.3.0",
|
||||||
"react-qr-barcode-scanner": "^1.0.6",
|
"react-qr-barcode-scanner": "^1.0.6",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
|
@@ -58,7 +58,6 @@ import DrawerSubViewProvider from "./providers/drawer-sub-view-provider";
|
|||||||
import CommunitiesHomeView from "./views/communities";
|
import CommunitiesHomeView from "./views/communities";
|
||||||
import CommunityFindByNameView from "./views/community/find-by-name";
|
import CommunityFindByNameView from "./views/community/find-by-name";
|
||||||
import CommunityView from "./views/community/index";
|
import CommunityView from "./views/community/index";
|
||||||
import StreamModerationView from "./views/tools/stream-moderation";
|
|
||||||
import PopularRelaysView from "./views/relays/popular";
|
import PopularRelaysView from "./views/relays/popular";
|
||||||
|
|
||||||
const NetworkView = React.lazy(() => import("./views/tools/network"));
|
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 StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||||
const SearchView = React.lazy(() => import("./views/search"));
|
const SearchView = React.lazy(() => import("./views/search"));
|
||||||
const MapView = React.lazy(() => import("./views/map"));
|
const MapView = React.lazy(() => import("./views/map"));
|
||||||
|
const StreamModerationView = React.lazy(() => import("./views/tools/stream-moderation"));
|
||||||
|
|
||||||
const overrideReactTextareaAutocompleteStyles = css`
|
const overrideReactTextareaAutocompleteStyles = css`
|
||||||
.rta__autocomplete {
|
.rta__autocomplete {
|
||||||
@@ -127,6 +127,7 @@ const router = createHashRouter([
|
|||||||
</PageProviders>
|
</PageProviders>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{ path: "tools/stream-moderation", element: <StreamModerationView /> },
|
||||||
{
|
{
|
||||||
path: "map",
|
path: "map",
|
||||||
element: <MapView />,
|
element: <MapView />,
|
||||||
@@ -181,7 +182,6 @@ const router = createHashRouter([
|
|||||||
{ path: "", element: <ToolsHomeView /> },
|
{ path: "", element: <ToolsHomeView /> },
|
||||||
{ path: "network", element: <NetworkView /> },
|
{ path: "network", element: <NetworkView /> },
|
||||||
{ path: "network-graph", element: <NetworkGraphView /> },
|
{ 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 Hls from "hls.js";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
@@ -13,14 +13,14 @@ export function LiveVideoPlayer({
|
|||||||
autoPlay,
|
autoPlay,
|
||||||
poster,
|
poster,
|
||||||
...props
|
...props
|
||||||
}: FlexProps & { stream?: string; autoPlay?: boolean; poster?: string }) {
|
}: Omit<BoxProps, "children"> & { stream?: string; autoPlay?: boolean; poster?: string }) {
|
||||||
const video = useRef<HTMLVideoElement>(null);
|
const video = useRef<HTMLVideoElement>(null);
|
||||||
const [status, setStatus] = useState<VideoStatus>();
|
const [status, setStatus] = useState<VideoStatus>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stream && video.current && !video.current.src && Hls.isSupported()) {
|
if (stream && video.current && !video.current.src && Hls.isSupported()) {
|
||||||
try {
|
try {
|
||||||
const hls = new Hls();
|
const hls = new Hls({ capLevelToPlayerSize: true });
|
||||||
hls.loadSource(stream);
|
hls.loadSource(stream);
|
||||||
hls.attachMedia(video.current);
|
hls.attachMedia(video.current);
|
||||||
hls.on(Hls.Events.ERROR, (event, data) => {
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
||||||
@@ -43,15 +43,15 @@ export function LiveVideoPlayer({
|
|||||||
}, [video, stream]);
|
}, [video, stream]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justifyContent="center" alignItems="center" {...props} position="relative">
|
<Box
|
||||||
<video
|
as="video"
|
||||||
ref={video}
|
ref={video}
|
||||||
playsInline={true}
|
playsInline={true}
|
||||||
controls={status === VideoStatus.Online}
|
controls={status === VideoStatus.Online}
|
||||||
autoPlay={autoPlay}
|
autoPlay={autoPlay}
|
||||||
poster={poster}
|
poster={poster}
|
||||||
style={{ maxHeight: "100%", maxWidth: "100%", width: "100%" }}
|
style={{ maxHeight: "100%", maxWidth: "100%", width: "100%" }}
|
||||||
/>
|
{...props}
|
||||||
</Flex>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ import { MagicInput, RefType } from "../../../../components/magic-textarea";
|
|||||||
import StreamZapButton from "../../components/stream-zap-button";
|
import StreamZapButton from "../../components/stream-zap-button";
|
||||||
import { nostrBuildUploadImage } from "../../../../helpers/nostr-build";
|
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 toast = useToast();
|
||||||
const emojis = useContextEmojis();
|
const emojis = useContextEmojis();
|
||||||
const streamRelays = useRelaySelectionRelays();
|
const streamRelays = useRelaySelectionRelays();
|
||||||
@@ -85,7 +85,7 @@ export default function ChatMessageForm({ stream }: { stream: ParsedStream }) {
|
|||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
<StreamZapButton stream={stream} onZap={reset} initComment={getValues().content} />
|
{!hideZapButton && <StreamZapButton stream={stream} onZap={reset} initComment={getValues().content} />}
|
||||||
</Box>
|
</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:
|
dependencies:
|
||||||
regenerator-runtime "^0.14.0"
|
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":
|
"@babel/template@^7.22.15", "@babel/template@^7.22.5":
|
||||||
version "7.22.15"
|
version "7.22.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
|
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"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
|
||||||
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
|
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":
|
"@remix-run/router@1.9.0":
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.9.0.tgz#9033238b41c4cbe1e961eccb3f79e2c588328cf6"
|
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"
|
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
|
||||||
integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
|
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:
|
clean-stack@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
|
||||||
@@ -3758,6 +3785,20 @@ dir-glob@^3.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-type "^4.0.0"
|
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:
|
dom-accessibility-api@^0.5.9:
|
||||||
version "0.5.16"
|
version "0.5.16"
|
||||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
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"
|
resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.4.12.tgz#2022daa29d10c662387d80a5297f8330f8ef5ee2"
|
||||||
integrity sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA==
|
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"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
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"
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
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:
|
import-fresh@^3.2.1:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
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"
|
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||||
integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
|
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"
|
version "15.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||||
@@ -5765,6 +5811,15 @@ randombytes@^2.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.1.0"
|
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:
|
react-clientside-effect@^1.2.6:
|
||||||
version "1.2.6"
|
version "1.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
|
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:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.13"
|
"@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:
|
react-dom@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
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"
|
fromentries "^1.3.2"
|
||||||
jerrypick "^1.1.1"
|
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:
|
react-photo-album@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-photo-album/-/react-photo-album-2.3.0.tgz#262afa60691d8ed5e25b8c8a73cec339ec515652"
|
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"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.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:
|
regenerate-unicode-properties@^10.1.0:
|
||||||
version "10.1.1"
|
version "10.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480"
|
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"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
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:
|
validate-npm-package-license@^3.0.1:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||||
|
Reference in New Issue
Block a user