mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 21:29:26 +02:00
Merge branch 'next'
This commit is contained in:
commit
b032922839
14
.github/workflows/docker-image.yml
vendored
14
.github/workflows/docker-image.yml
vendored
@ -1,9 +1,7 @@
|
||||
name: Create and publish a Docker image
|
||||
# copied from https://blog.pradumnasaraf.dev/publish-image-on-ghcr
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
on: push
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@ -32,10 +30,18 @@ jobs:
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=schedule
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
33
.github/workflows/release.yml
vendored
Normal file
33
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js 18
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: changesets/action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@ -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",
|
||||
|
11
src/app.tsx
11
src/app.tsx
@ -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,14 @@ const router = createHashRouter([
|
||||
</PageProviders>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "tools/stream-moderation",
|
||||
element: (
|
||||
<PageProviders>
|
||||
<StreamModerationView />
|
||||
</PageProviders>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "map",
|
||||
element: <MapView />,
|
||||
@ -181,7 +189,6 @@ const router = createHashRouter([
|
||||
{ path: "", element: <ToolsHomeView /> },
|
||||
{ path: "network", element: <NetworkView /> },
|
||||
{ path: "network-graph", element: <NetworkGraphView /> },
|
||||
{ path: "stream-moderation", element: <StreamModerationView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -66,7 +66,7 @@ export default function EventReactionButtons({ event, max }: { event: NostrEvent
|
||||
url={group.url}
|
||||
count={group.count}
|
||||
onClick={() => addReaction(group.emoji, group.url)}
|
||||
colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "brand" : undefined}
|
||||
colorScheme={account && group.pubkeys.includes(account?.pubkey) ? "primary" : undefined}
|
||||
/>
|
||||
))}
|
||||
<Button onClick={detailsModal.onOpen}>Show all</Button>
|
||||
|
@ -206,9 +206,9 @@ export const QrCodeIcon = createIcon({
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const ChatIcon = createIcon({
|
||||
export const MessagesIcon = createIcon({
|
||||
displayName: "ChatIcon",
|
||||
d: "M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8zm2 14h2a6 6 0 1 0 0-12h-4a6 6 0 0 0-6 6c0 3.61 2.462 5.966 8 8.48V17z",
|
||||
d: "M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM20 7.23792L12.0718 14.338L4 7.21594V19H20V7.23792ZM4.51146 5L12.0619 11.662L19.501 5H4.51146Z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
@ -254,6 +254,12 @@ export const EditIcon = createIcon({
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const WritingIcon = createIcon({
|
||||
displayName: "WritingIcon",
|
||||
d: "M6.93912 14.0327C6.7072 14.6562 6.51032 15.233 6.33421 15.8154C7.29345 15.1188 8.43544 14.6766 9.75193 14.512C12.2652 14.1979 14.4976 12.5384 15.6279 10.4535L14.1721 8.99878L15.5848 7.58407C15.9185 7.24993 16.2521 6.91603 16.5858 6.58237C17.0151 6.15301 17.5 5.35838 18.0129 4.21479C12.4197 5.08172 8.99484 8.50636 6.93912 14.0327ZM17 8.99728L18 9.99658C17 12.9966 14 15.9966 10 16.4966C7.33146 16.8301 5.66421 18.6635 4.99824 21.9966H3C4 15.9966 6 1.99658 21 1.99658C20.0009 4.99392 19.0018 6.99303 18.0027 7.99391C17.6662 8.33038 17.3331 8.66372 17 8.99728Z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const AtIcon = createIcon({
|
||||
displayName: "AtIcon",
|
||||
d: "M20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C13.6418 20 15.1681 19.5054 16.4381 18.6571L17.5476 20.3214C15.9602 21.3818 14.0523 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12V13.5C22 15.433 20.433 17 18.5 17C17.2958 17 16.2336 16.3918 15.6038 15.4659C14.6942 16.4115 13.4158 17 12 17C9.23858 17 7 14.7614 7 12C7 9.23858 9.23858 7 12 7C13.1258 7 14.1647 7.37209 15.0005 8H17V13.5C17 14.3284 17.6716 15 18.5 15C19.3284 15 20 14.3284 20 13.5V12ZM12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9Z",
|
||||
|
@ -1,43 +1,34 @@
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
Text,
|
||||
useAccordionContext,
|
||||
} from "@chakra-ui/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Box, Button, Flex, IconButton, Text, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import accountService, { Account } from "../../services/account";
|
||||
import { AddIcon } from "../icons";
|
||||
import { AddIcon, ArrowDownSIcon, ArrowUpSIcon } from "../icons";
|
||||
import { UserAvatar } from "../user-avatar";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import AccountInfoBadge from "../account-info-badge";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
|
||||
function AccountItem({ account }: { account: Account }) {
|
||||
function AccountItem({ account, onClick }: { account: Account; onClick?: () => void }) {
|
||||
const pubkey = account.pubkey;
|
||||
const metadata = useUserMetadata(pubkey, []);
|
||||
const accord = useAccordionContext();
|
||||
|
||||
const handleClick = () => {
|
||||
if (accord) accord.setIndex(-1);
|
||||
accountService.switchAccount(pubkey);
|
||||
if (onClick) onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" gap="2" alignItems="center" cursor="pointer" onClick={handleClick}>
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
<Box flex={1} overflow="hidden">
|
||||
<Text isTruncated>{getUserDisplayName(metadata, pubkey)}</Text>
|
||||
<AccountInfoBadge fontSize="0.7em" account={account} />
|
||||
</Box>
|
||||
<Box display="flex" gap="2" alignItems="center" cursor="pointer">
|
||||
<Flex as="button" onClick={handleClick} flex={1} gap="2">
|
||||
<UserAvatar pubkey={pubkey} size="md" />
|
||||
<Flex overflow="hidden" direction="column" alignItems="flex-start">
|
||||
<Text isTruncated>{getUserDisplayName(metadata, pubkey)}</Text>
|
||||
<AccountInfoBadge fontSize="0.7em" account={account} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
aria-label="Remove Account"
|
||||
@ -52,49 +43,53 @@ function AccountItem({ account }: { account: Account }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function AccountSwitcherList() {
|
||||
export default function AccountSwitcher() {
|
||||
const navigate = useNavigate();
|
||||
const account = useCurrentAccount()!;
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
const metadata = useUserMetadata(account.pubkey);
|
||||
const accounts = useSubject(accountService.accounts);
|
||||
const current = useSubject(accountService.current);
|
||||
const location = useLocation();
|
||||
|
||||
const otherAccounts = accounts.filter((acc) => acc.pubkey !== current?.pubkey);
|
||||
const otherAccounts = accounts.filter((acc) => acc.pubkey !== account?.pubkey);
|
||||
|
||||
return (
|
||||
<Flex gap="2" direction="column" padding="2">
|
||||
{otherAccounts.map((account) => (
|
||||
<AccountItem key={account.pubkey} account={account} />
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
leftIcon={<AddIcon />}
|
||||
onClick={() => {
|
||||
accountService.logout();
|
||||
navigate("/login", { state: { from: location.pathname } });
|
||||
}}
|
||||
<Flex direction="column" gap="2">
|
||||
<Box
|
||||
as="button"
|
||||
borderRadius="30"
|
||||
borderWidth={1}
|
||||
display="flex"
|
||||
gap="2"
|
||||
mb="2"
|
||||
alignItems="center"
|
||||
flexGrow={1}
|
||||
overflow="hidden"
|
||||
onClick={onToggle}
|
||||
>
|
||||
Add Account
|
||||
</Button>
|
||||
<UserAvatar pubkey={account.pubkey} noProxy size="md" />
|
||||
<Text whiteSpace="nowrap" fontWeight="bold" fontSize="lg" isTruncated>
|
||||
{getUserDisplayName(metadata, account.pubkey)}
|
||||
</Text>
|
||||
<Flex ml="auto" alignItems="center" justifyContent="center" aspectRatio={1} h="3rem">
|
||||
{isOpen ? <ArrowUpSIcon fontSize="1.5rem" /> : <ArrowDownSIcon fontSize="1.5rem" />}
|
||||
</Flex>
|
||||
</Box>
|
||||
{isOpen && (
|
||||
<>
|
||||
{otherAccounts.map((account) => (
|
||||
<AccountItem key={account.pubkey} account={account} onClick={onClose} />
|
||||
))}
|
||||
<Button
|
||||
leftIcon={<AddIcon />}
|
||||
onClick={() => {
|
||||
accountService.logout();
|
||||
navigate("/login", { state: { from: location.pathname } });
|
||||
}}
|
||||
>
|
||||
Add Account
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AccountSwitcher() {
|
||||
return (
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Accounts
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel padding={0}>
|
||||
<AccountSwitcherList />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { useContext } from "react";
|
||||
import { Avatar, Button, Flex, FlexProps, Heading, IconButton, LinkOverlay, Text } from "@chakra-ui/react";
|
||||
import { Avatar, Box, Button, Flex, FlexProps, Heading, LinkOverlay } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import accountService from "../../services/account";
|
||||
import { EditIcon, LogoutIcon } from "../icons";
|
||||
import ProfileButton from "./profile-button";
|
||||
import AccountSwitcher from "./account-switcher";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import PublishLog from "../publish-log";
|
||||
import NavItems from "./nav-items";
|
||||
import { css } from "@emotion/react";
|
||||
import { useContext } from "react";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { WritingIcon } from "../icons";
|
||||
|
||||
const hideScrollbar = css`
|
||||
-ms-overflow-style: none;
|
||||
@ -39,53 +37,36 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
||||
css={hideScrollbar}
|
||||
>
|
||||
<Flex direction="column" flexShrink={0} gap="2">
|
||||
<Flex gap="2" alignItems="center" position="relative">
|
||||
<LinkOverlay as={RouterLink} to="/" />
|
||||
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||
<Heading size="md">noStrudel</Heading>
|
||||
<Flex gap="2" alignItems="center" position="relative" my="2">
|
||||
<Avatar src="/apple-touch-icon.png" size="md" />
|
||||
<Heading size="md">
|
||||
<LinkOverlay as={RouterLink} to="/">
|
||||
noStrudel
|
||||
</LinkOverlay>
|
||||
</Heading>
|
||||
</Flex>
|
||||
<Flex gap="2" overflow="hidden">
|
||||
{account ? (
|
||||
<>
|
||||
<ProfileButton />
|
||||
{!account.readonly && (
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
aria-label="New note"
|
||||
title="New note"
|
||||
w="3rem"
|
||||
h="3rem"
|
||||
fontSize="1.5rem"
|
||||
colorScheme="brand"
|
||||
onClick={() => openModal()}
|
||||
flexShrink={0}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button as={RouterLink} to="/login" state={{ from: location.pathname }} colorScheme="brand" w="full">
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<AccountSwitcher />
|
||||
<NavItems />
|
||||
{account && (
|
||||
<Button
|
||||
onClick={() => accountService.logout()}
|
||||
leftIcon={<LogoutIcon />}
|
||||
variant="link"
|
||||
justifyContent="flex-start"
|
||||
pl="2"
|
||||
py="2"
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
<>
|
||||
<AccountSwitcher />
|
||||
<Button
|
||||
leftIcon={<WritingIcon />}
|
||||
aria-label="Write Note"
|
||||
title="Write Note"
|
||||
onClick={() => openModal()}
|
||||
colorScheme="primary"
|
||||
size="lg"
|
||||
isDisabled={account.readonly}
|
||||
>
|
||||
Write Note
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{account?.readonly && (
|
||||
<Text color="red.200" textAlign="center">
|
||||
Readonly Mode
|
||||
</Text>
|
||||
<NavItems />
|
||||
<Box h="4" />
|
||||
{!account && (
|
||||
<Button as={RouterLink} to="/login" state={{ from: location.pathname }} colorScheme="primary" w="full">
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<PublishLog overflowY="auto" minH="15rem" my="4" />
|
||||
|
@ -3,7 +3,7 @@ import { useContext, useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { ChatIcon, FeedIcon, HomeIcon, NotificationIcon, PlusCircleIcon, SearchIcon } from "../icons";
|
||||
import { MessagesIcon, FeedIcon, HomeIcon, NotificationIcon, PlusCircleIcon, SearchIcon } from "../icons";
|
||||
import { UserAvatar } from "../user-avatar";
|
||||
import MobileSideDrawer from "./mobile-side-drawer";
|
||||
|
||||
@ -39,10 +39,16 @@ export default function MobileBottomNav(props: Omit<FlexProps, "children">) {
|
||||
openModal();
|
||||
}}
|
||||
variant="solid"
|
||||
colorScheme="brand"
|
||||
colorScheme="primary"
|
||||
isDisabled={account?.readonly ?? true}
|
||||
/>
|
||||
<IconButton icon={<ChatIcon />} aria-label="Messages" onClick={() => navigate(`/dm`)} flexGrow="1" size="md" />
|
||||
<IconButton
|
||||
icon={<MessagesIcon />}
|
||||
aria-label="Messages"
|
||||
onClick={() => navigate(`/dm`)}
|
||||
flexGrow="1"
|
||||
size="md"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<NotificationIcon />}
|
||||
aria-label="Notifications"
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerBody,
|
||||
DrawerCloseButton,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerOverlay,
|
||||
@ -13,12 +13,8 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { LogoutIcon } from "../icons";
|
||||
import { UserAvatar } from "../user-avatar";
|
||||
import { UserLink } from "../user-link";
|
||||
import AccountSwitcher from "./account-switcher";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import accountService from "../../services/account";
|
||||
import NavItems from "./nav-items";
|
||||
|
||||
export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "children">) {
|
||||
@ -28,42 +24,31 @@ export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "childr
|
||||
<Drawer placement="left" {...props}>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerCloseButton />
|
||||
<DrawerHeader px="2" py="4">
|
||||
<DrawerBody
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
px="4"
|
||||
pt="4"
|
||||
pb="8"
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
gap="2"
|
||||
>
|
||||
{account ? (
|
||||
<Flex gap="2">
|
||||
<UserAvatar pubkey={account.pubkey} size="sm" noProxy />
|
||||
<UserLink pubkey={account.pubkey} />
|
||||
</Flex>
|
||||
<AccountSwitcher />
|
||||
) : (
|
||||
<Flex gap="2">
|
||||
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||
<Flex gap="2" my="2" alignItems="center">
|
||||
<Avatar src="/apple-touch-icon.png" size="md" />
|
||||
<Text m={0}>Nostrudel</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</DrawerHeader>
|
||||
<DrawerBody padding={0} overflowY="auto" overflowX="hidden">
|
||||
{account && <AccountSwitcher />}
|
||||
<Flex direction="column" gap="2" padding="2">
|
||||
{!account && (
|
||||
<Button as={RouterLink} to="/login" colorScheme="brand">
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
<NavItems />
|
||||
{account && (
|
||||
<Button
|
||||
onClick={() => accountService.logout()}
|
||||
leftIcon={<LogoutIcon />}
|
||||
justifyContent="flex-start"
|
||||
variant="link"
|
||||
pl="2"
|
||||
py="2"
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<NavItems />
|
||||
<Box h="2" />
|
||||
{!account && (
|
||||
<Button as={RouterLink} to="/login" colorScheme="primary">
|
||||
Login
|
||||
</Button>
|
||||
)}
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
@ -2,7 +2,7 @@ import { AbsoluteCenter, Box, Button, ButtonProps, Divider, Text } from "@chakra
|
||||
import { useLoaderData, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
BadgeIcon,
|
||||
ChatIcon,
|
||||
MessagesIcon,
|
||||
CommunityIcon,
|
||||
EmojiIcon,
|
||||
FeedIcon,
|
||||
@ -10,12 +10,16 @@ import {
|
||||
ListIcon,
|
||||
LiveStreamIcon,
|
||||
NotificationIcon,
|
||||
ProfileIcon,
|
||||
RelayIcon,
|
||||
SearchIcon,
|
||||
SettingsIcon,
|
||||
ToolsIcon,
|
||||
LogoutIcon,
|
||||
} from "../icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import accountService from "../../services/account";
|
||||
|
||||
export default function NavItems() {
|
||||
const navigate = useNavigate();
|
||||
@ -24,7 +28,6 @@ export default function NavItems() {
|
||||
|
||||
const buttonProps: ButtonProps = {
|
||||
py: "2",
|
||||
pl: "2",
|
||||
justifyContent: "flex-start",
|
||||
variant: "link",
|
||||
};
|
||||
@ -49,7 +52,7 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/")}
|
||||
leftIcon={<FeedIcon />}
|
||||
colorScheme={active === "notes" ? "brand" : undefined}
|
||||
colorScheme={active === "notes" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Notes
|
||||
@ -59,15 +62,15 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/notifications")}
|
||||
leftIcon={<NotificationIcon />}
|
||||
colorScheme={active === "notifications" ? "brand" : undefined}
|
||||
colorScheme={active === "notifications" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Notifications
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate("/dm")}
|
||||
leftIcon={<ChatIcon />}
|
||||
colorScheme={active === "dm" ? "brand" : undefined}
|
||||
leftIcon={<MessagesIcon />}
|
||||
colorScheme={active === "dm" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Messages
|
||||
@ -77,15 +80,24 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/search")}
|
||||
leftIcon={<SearchIcon />}
|
||||
colorScheme={active === "search" ? "brand" : undefined}
|
||||
colorScheme={active === "search" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
{account?.pubkey && (
|
||||
<Button
|
||||
onClick={() => navigate("/u/" + nip19.npubEncode(account.pubkey))}
|
||||
leftIcon={<ProfileIcon />}
|
||||
{...buttonProps}
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => navigate("/relays")}
|
||||
leftIcon={<RelayIcon />}
|
||||
colorScheme={active === "relays" ? "brand" : undefined}
|
||||
colorScheme={active === "relays" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Relays
|
||||
@ -96,7 +108,7 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/streams")}
|
||||
leftIcon={<LiveStreamIcon />}
|
||||
colorScheme={active === "streams" ? "brand" : undefined}
|
||||
colorScheme={active === "streams" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Streams
|
||||
@ -104,7 +116,7 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/communities")}
|
||||
leftIcon={<CommunityIcon />}
|
||||
colorScheme={active === "communities" ? "brand" : undefined}
|
||||
colorScheme={active === "communities" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Communities
|
||||
@ -112,7 +124,7 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/lists")}
|
||||
leftIcon={<ListIcon />}
|
||||
colorScheme={active === "lists" ? "brand" : undefined}
|
||||
colorScheme={active === "lists" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Lists
|
||||
@ -120,7 +132,7 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/goals")}
|
||||
leftIcon={<GoalIcon />}
|
||||
colorScheme={active === "goals" ? "brand" : undefined}
|
||||
colorScheme={active === "goals" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Goals
|
||||
@ -128,7 +140,7 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/badges")}
|
||||
leftIcon={<BadgeIcon />}
|
||||
colorScheme={active === "badges" ? "brand" : undefined}
|
||||
colorScheme={active === "badges" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Badges
|
||||
@ -136,7 +148,7 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/emojis")}
|
||||
leftIcon={<EmojiIcon />}
|
||||
colorScheme={active === "emojis" ? "brand" : undefined}
|
||||
colorScheme={active === "emojis" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Emojis
|
||||
@ -144,20 +156,25 @@ export default function NavItems() {
|
||||
<Button
|
||||
onClick={() => navigate("/tools")}
|
||||
leftIcon={<ToolsIcon />}
|
||||
colorScheme={active === "tools" ? "brand" : undefined}
|
||||
colorScheme={active === "tools" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Tools
|
||||
</Button>
|
||||
<Divider my="2" />
|
||||
<Box h="4" />
|
||||
<Button
|
||||
onClick={() => navigate("/settings")}
|
||||
leftIcon={<SettingsIcon />}
|
||||
colorScheme={active === "settings" ? "brand" : undefined}
|
||||
colorScheme={active === "settings" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
{account && (
|
||||
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />} {...buttonProps}>
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
import { LinkBox, LinkOverlay } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { UserAvatar } from "../user-avatar";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
|
||||
export default function ProfileButton() {
|
||||
const account = useCurrentAccount()!;
|
||||
const metadata = useUserMetadata(account.pubkey);
|
||||
|
||||
return (
|
||||
<LinkBox
|
||||
borderRadius="lg"
|
||||
borderWidth={1}
|
||||
p="2"
|
||||
display="flex"
|
||||
gap="2"
|
||||
alignItems="center"
|
||||
flexGrow={1}
|
||||
overflow="hidden"
|
||||
>
|
||||
<UserAvatar pubkey={account.pubkey} noProxy size="sm" />
|
||||
<LinkOverlay
|
||||
as={RouterLink}
|
||||
to={`/u/${nip19.npubEncode(account.pubkey)}`}
|
||||
whiteSpace="nowrap"
|
||||
fontWeight="bold"
|
||||
fontSize="lg"
|
||||
title="View profile"
|
||||
isTruncated
|
||||
>
|
||||
{getUserDisplayName(metadata, account.pubkey)}
|
||||
</LinkOverlay>
|
||||
</LinkBox>
|
||||
);
|
||||
}
|
@ -109,7 +109,7 @@ function EventSlideHeader({ event, ...props }: { event: NostrEvent } & Omit<Flex
|
||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<Spacer />
|
||||
<Button as={RouterLink} to={`/n/${encoded}`} colorScheme="brand" size="sm">
|
||||
<Button as={RouterLink} to={`/n/${encoded}`} colorScheme="primary" size="sm">
|
||||
View Note
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Badge, Flex, FlexProps } from "@chakra-ui/react";
|
||||
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,15 +12,21 @@ export function LiveVideoPlayer({
|
||||
stream,
|
||||
autoPlay,
|
||||
poster,
|
||||
muted,
|
||||
...props
|
||||
}: FlexProps & { 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>();
|
||||
|
||||
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 +49,16 @@ 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}
|
||||
muted={muted}
|
||||
style={{ maxHeight: "100%", maxWidth: "100%", width: "100%" }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ export function RepostButton({ event }: { event: NostrEvent }) {
|
||||
<Button variant="ghost" size="sm" mr={2} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" variant="solid" onClick={handleClick} size="sm" isLoading={loading}>
|
||||
<Button colorScheme="primary" variant="solid" onClick={handleClick} size="sm" isLoading={loading}>
|
||||
Repost
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@ -38,7 +38,7 @@ import BookmarkButton from "./components/bookmark-button";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import NoteReactions from "./components/note-reactions";
|
||||
import ReplyForm from "../../views/note/components/reply-form";
|
||||
import { getEventCoordinate, getReferences, parseCoordinate } from "../../helpers/nostr/events";
|
||||
import { getReferences, parseCoordinate } from "../../helpers/nostr/events";
|
||||
import Timestamp from "../timestamp";
|
||||
import OpenInDrawerButton from "../open-in-drawer-button";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
@ -136,7 +136,7 @@ export const Note = React.memo(
|
||||
icon={<ExternalLinkIcon />}
|
||||
aria-label="Open External"
|
||||
href={externalLink}
|
||||
size="sm"
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
target="_blank"
|
||||
/>
|
||||
|
@ -43,7 +43,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, .
|
||||
leftIcon={<LightningIcon />}
|
||||
aria-label="Zap Note"
|
||||
title="Zap Note"
|
||||
colorScheme={hasZapped ? "brand" : undefined}
|
||||
colorScheme={hasZapped ? "primary" : undefined}
|
||||
{...props}
|
||||
onClick={onOpen}
|
||||
isDisabled={!metadata?.allowsNostr}
|
||||
|
@ -67,7 +67,7 @@ export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
||||
/>
|
||||
<Flex>
|
||||
<Input placeholder="🔥" display="inline" size="sm" minW="2rem" w="5rem" />
|
||||
<Button variant="solid" colorScheme="brand" size="sm">
|
||||
<Button variant="solid" colorScheme="primary" size="sm">
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -102,7 +102,7 @@ export default function RelaySelectionModal({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="brand"
|
||||
colorScheme="primary"
|
||||
onClick={() => {
|
||||
onSubmit(newSelected);
|
||||
onClose();
|
||||
|
@ -106,7 +106,7 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
|
||||
<Box h="0" overflow="visible" w="full" zIndex={100} display="flex" position="relative">
|
||||
<Button
|
||||
onClick={() => setLatest(timeline.timeline.value[0].created_at + 10)}
|
||||
colorScheme="brand"
|
||||
colorScheme="primary"
|
||||
size="lg"
|
||||
mx="auto"
|
||||
w={["50%", null, "30%"]}
|
||||
|
@ -20,7 +20,7 @@ export default function TimelineActionAndStatus({ timeline }: { timeline: Timeli
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={() => timeline.loadMore()} flexShrink={0} size="lg" mx="auto" colorScheme="brand" my="4">
|
||||
<Button onClick={() => timeline.loadMore()} flexShrink={0} size="lg" mx="auto" colorScheme="primary" my="4">
|
||||
Load More
|
||||
</Button>
|
||||
);
|
||||
|
@ -1,19 +1,27 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
|
||||
const aDayAgo = dayjs().subtract(1, "day");
|
||||
|
||||
export default function Timestamp({ timestamp, ...props }: { timestamp: number } & Omit<BoxProps, "children">) {
|
||||
const date = dayjs.unix(timestamp);
|
||||
const now = dayjs();
|
||||
|
||||
let display = date.format("L");
|
||||
|
||||
if (now.diff(date, "week") <= 2) {
|
||||
if (now.diff(date, "d") >= 1) {
|
||||
display = Math.round(now.diff(date, "d") * 10) / 10 + `d`;
|
||||
} else if (now.diff(date, "h") >= 1) {
|
||||
display = Math.round(now.diff(date, "h")) + `h`;
|
||||
} else if (now.diff(date, "m") >= 1) {
|
||||
display = Math.round(now.diff(date, "m")) + `m`;
|
||||
} else if (now.diff(date, "s") >= 1) {
|
||||
display = Math.round(now.diff(date, "s")) + `s`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="time"
|
||||
dateTime={date.toISOString()}
|
||||
title={date.isBefore(aDayAgo) ? date.fromNow() : date.format("LLL")}
|
||||
{...props}
|
||||
>
|
||||
{date.isBefore(aDayAgo) ? date.format("L LT") : date.fromNow()}
|
||||
<Box as="time" dateTime={date.toISOString()} title={date.format("LLL")} {...props}>
|
||||
{display}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ export const UserFollowButton = ({ pubkey, showLists, ...props }: UserFollowButt
|
||||
if (showLists) {
|
||||
return (
|
||||
<Menu closeOnSelect={false}>
|
||||
<MenuButton as={Button} colorScheme="brand" {...props} rightIcon={<ArrowDownSIcon />} isDisabled={isDisabled}>
|
||||
<MenuButton as={Button} colorScheme="primary" {...props} rightIcon={<ArrowDownSIcon />} isDisabled={isDisabled}>
|
||||
{isFollowing ? "Unfollow" : "Follow"}
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
@ -174,13 +174,13 @@ export const UserFollowButton = ({ pubkey, showLists, ...props }: UserFollowButt
|
||||
);
|
||||
} else if (isFollowing) {
|
||||
return (
|
||||
<Button onClick={handleUnfollow} colorScheme="brand" icon={<UnfollowIcon />} isDisabled={isDisabled} {...props}>
|
||||
<Button onClick={handleUnfollow} colorScheme="primary" icon={<UnfollowIcon />} isDisabled={isDisabled} {...props}>
|
||||
Unfollow
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button onClick={handleFollow} colorScheme="brand" icon={<FollowIcon />} isDisabled={isDisabled} {...props}>
|
||||
<Button onClick={handleFollow} colorScheme="primary" icon={<FollowIcon />} isDisabled={isDisabled} {...props}>
|
||||
Follow
|
||||
</Button>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ export const getMatchNostrLink = () =>
|
||||
export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}]+)/gu;
|
||||
export const getMatchLink = () =>
|
||||
/https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{Letter}\p{Number}&\.-\/\?=#\-@%\+_,:!]*)/gu;
|
||||
export const getMatchEmoji = () => /:([a-zA-Z0-9_]+):/gi;
|
||||
export const getMatchEmoji = () => /:([a-zA-Z0-9_-]+):/gi;
|
||||
|
||||
// read more https://www.regular-expressions.info/unicode.html#category
|
||||
export function stripInvisibleChar(str?: string) {
|
||||
|
@ -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="primary" 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>
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export default function RequireCurrentAccount({ children }: { children: JSX.Elem
|
||||
as={Link}
|
||||
to="/login"
|
||||
state={{ from: location.pathname }}
|
||||
colorScheme="brand"
|
||||
colorScheme="primary"
|
||||
rightIcon={<ExternalLinkIcon />}
|
||||
>
|
||||
Login
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { extendTheme } from "@chakra-ui/react";
|
||||
import { extendTheme, Theme, DeepPartial } from "@chakra-ui/react";
|
||||
import { containerTheme } from "./container";
|
||||
|
||||
const breakpoints = ["sm", "md", "lg", "xl", "2xl"] as const;
|
||||
@ -6,7 +6,7 @@ const breakpoints = ["sm", "md", "lg", "xl", "2xl"] as const;
|
||||
export default function createTheme(primaryColor: string = "#8DB600", maxBreakpoint?: (typeof breakpoints)[number]) {
|
||||
const theme = extendTheme({
|
||||
colors: {
|
||||
brand: {
|
||||
primary: {
|
||||
50: primaryColor,
|
||||
100: primaryColor,
|
||||
200: primaryColor,
|
||||
@ -22,7 +22,7 @@ export default function createTheme(primaryColor: string = "#8DB600", maxBreakpo
|
||||
components: {
|
||||
Container: containerTheme,
|
||||
},
|
||||
});
|
||||
} as DeepPartial<Theme>);
|
||||
|
||||
// if maxBreakpoint is set, set all breakpoints above it to a large number so they are never reached
|
||||
if (maxBreakpoint && breakpoints.includes(maxBreakpoint)) {
|
||||
|
@ -70,7 +70,7 @@ export default function EmojiPackCreateModal({ onClose, ...props }: Omit<ModalPr
|
||||
<Button mr="2" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" type="submit">
|
||||
<Button colorScheme="primary" type="submit">
|
||||
Create
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
@ -53,7 +53,13 @@ function AddEmojiForm({ onAdd }: { onAdd: (values: { name: string; url: string }
|
||||
|
||||
return (
|
||||
<Flex as="form" gap="2" onSubmit={submit}>
|
||||
<Input placeholder="name" {...register("name", { required: true })} autoComplete="off" />
|
||||
<Input
|
||||
placeholder="name"
|
||||
{...register("name", { required: true })}
|
||||
pattern="^[a-zA-Z0-9_-]+$"
|
||||
autoComplete="off"
|
||||
title="emoji name, can not contain spaces"
|
||||
/>
|
||||
<Input placeholder="https://example.com/emoji.png" {...register("url", { required: true })} autoComplete="off" />
|
||||
{previewURL && <Image aspectRatio={1} h="10" src={previewURL} />}
|
||||
<Button flexShrink={0} type="submit">
|
||||
@ -132,7 +138,7 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) {
|
||||
{isAuthor && (
|
||||
<>
|
||||
{!editing && (
|
||||
<Button colorScheme="brand" onClick={startEdit}>
|
||||
<Button colorScheme="primary" onClick={startEdit}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
@ -184,7 +190,7 @@ function EmojiPackPage({ pack }: { pack: NostrEvent }) {
|
||||
<Button ml="auto" onClick={cancelEdit}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" onClick={saveEdit}>
|
||||
<Button colorScheme="primary" onClick={saveEdit}>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -81,7 +81,7 @@ export default function EmojiPacksView() {
|
||||
Emoji pack manager
|
||||
</Button>
|
||||
{account && (
|
||||
<Button colorScheme="brand" onClick={createModal.onOpen}>
|
||||
<Button colorScheme="primary" onClick={createModal.onOpen}>
|
||||
Create Emoji pack
|
||||
</Button>
|
||||
)}
|
||||
|
@ -92,7 +92,7 @@ export default function NewListModal({
|
||||
</FormControl>
|
||||
<ButtonGroup ml="auto">
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
||||
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
@ -39,7 +39,7 @@ function ListsPage() {
|
||||
>
|
||||
Listr
|
||||
</Button>
|
||||
<Button leftIcon={<PlusCircleIcon />} onClick={newList.onOpen} colorScheme="brand">
|
||||
<Button leftIcon={<PlusCircleIcon />} onClick={newList.onOpen} colorScheme="primary">
|
||||
New List
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -133,7 +133,7 @@ export default function LoginNip05View() {
|
||||
<Button variant="link" onClick={() => navigate("../")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button colorScheme="brand" ml="auto" type="submit" isDisabled={!pubkey}>
|
||||
<Button colorScheme="primary" ml="auto" type="submit" isDisabled={!pubkey}>
|
||||
Login
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -54,7 +54,7 @@ export default function LoginNpubView() {
|
||||
<Button variant="link" onClick={() => navigate("../")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button colorScheme="brand" ml="auto" type="submit">
|
||||
<Button colorScheme="primary" ml="auto" type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -146,7 +146,7 @@ export default function LoginNsecView() {
|
||||
<Button ml="auto" onClick={generateNewKey}>
|
||||
Generate New
|
||||
</Button>
|
||||
<Button colorScheme="brand" type="submit">
|
||||
<Button colorScheme="primary" type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -67,7 +67,7 @@ export default function LoginStartView() {
|
||||
<AlertDescription>There are bugs and things will break.</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
<Button onClick={loginWithExtension} colorScheme="brand">
|
||||
<Button onClick={loginWithExtension} colorScheme="primary">
|
||||
Use browser extension (NIP-07)
|
||||
</Button>
|
||||
<Button as={RouterLink} to="./nip05" state={location.state}>
|
||||
|
@ -156,7 +156,7 @@ export default function MapView() {
|
||||
<Button flexShrink={0} onClick={() => navigate(-1)}>
|
||||
Back
|
||||
</Button>
|
||||
<Button colorScheme="brand" onClick={setCellsFromMap} flex={1}>
|
||||
<Button colorScheme="primary" onClick={setCellsFromMap} flex={1}>
|
||||
Search this area
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -133,7 +133,7 @@ export default function ReplyForm({ item, onCancel, onSubmitted }: ReplyFormProp
|
||||
<UserAvatarStack label="Notify" pubkeys={notifyPubkeys} />
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button type="submit" colorScheme="brand" size="sm">
|
||||
<Button type="submit" colorScheme="primary" size="sm">
|
||||
Submit
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
@ -113,7 +113,7 @@ function NotificationsPage() {
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<Tabs isLazy colorScheme="brand">
|
||||
<Tabs isLazy colorScheme="primary">
|
||||
<TabList overflowX="auto" overflowY="hidden">
|
||||
<Tab>Replies</Tab>
|
||||
<Tab>Mentions</Tab>
|
||||
|
@ -16,13 +16,14 @@ import { EmbedEvent, EmbedEventPointer } from "../../components/embed-event";
|
||||
import EmbeddedUnknown from "../../components/embed-event/event-types/embedded-unknown";
|
||||
import { NoteContents } from "../../components/note/note-contents";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
|
||||
const Kind1Notification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ event }, ref) => {
|
||||
const refs = getReferences(event);
|
||||
|
||||
if (refs.replyId) {
|
||||
return (
|
||||
<Card variant="outline" p="2" borderColor="blue.400" ref={ref}>
|
||||
<Card variant="outline" p="2" ref={ref}>
|
||||
<Flex gap="2" alignItems="center" mb="2" wrap="wrap">
|
||||
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} />
|
||||
@ -118,7 +119,7 @@ const ZapNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>(({ eve
|
||||
}
|
||||
|
||||
return (
|
||||
<Card variant="outline" borderColor="yellow.400" p="2" ref={ref}>
|
||||
<Card variant="outline" p="2" ref={ref}>
|
||||
<Flex direction="row" gap="2" alignItems="center" mb="2">
|
||||
<UserAvatar pubkey={zap.request.pubkey} size="xs" />
|
||||
<UserLink pubkey={zap.request.pubkey} />
|
||||
@ -152,7 +153,13 @@ const NotificationItem = ({ event }: { event: NostrEvent }) => {
|
||||
content = <EmbeddedUnknown event={event} />;
|
||||
break;
|
||||
}
|
||||
return content && <ErrorBoundary>{content}</ErrorBoundary>;
|
||||
return (
|
||||
content && (
|
||||
<ErrorBoundary>
|
||||
<TrustProvider event={event}>{content}</TrustProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NotificationItem);
|
||||
|
@ -179,7 +179,7 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
|
||||
Download Backup
|
||||
</Button>
|
||||
<Button onClick={() => reset()}>Reset</Button>
|
||||
<Button colorScheme="brand" isLoading={isSubmitting} type="submit">
|
||||
<Button colorScheme="primary" isLoading={isSubmitting} type="submit">
|
||||
Update
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -94,7 +94,7 @@ export default function AddCustomRelayModal({
|
||||
<Button variant="ghost" onClick={props.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" onClick={() => safeUrl && onSubmit(safeUrl)} isDisabled={!safeUrl}>
|
||||
<Button colorScheme="primary" onClick={() => safeUrl && onSubmit(safeUrl)} isDisabled={!safeUrl}>
|
||||
Add
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
@ -49,7 +49,7 @@ export default function RelaysView() {
|
||||
<Button as={RouterLink} to="/relays/reviews">
|
||||
Browse Reviews
|
||||
</Button>
|
||||
<Button colorScheme="brand" onClick={addRelayModal.onOpen}>
|
||||
<Button colorScheme="primary" onClick={addRelayModal.onOpen}>
|
||||
Add Custom
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -65,7 +65,7 @@ function RelayPage({ relay }: { relay: string }) {
|
||||
</Flex>
|
||||
<RelayMetadata url={relay} extended />
|
||||
{info?.supported_nips && <SupportedNIPs nips={info?.supported_nips} />}
|
||||
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="brand">
|
||||
<Tabs display="flex" flexDirection="column" flexGrow="1" isLazy colorScheme="primary">
|
||||
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
||||
<Tab>Reviews</Tab>
|
||||
<Tab>Notes</Tab>
|
||||
@ -76,7 +76,7 @@ function RelayPage({ relay }: { relay: string }) {
|
||||
<Flex gap="2">
|
||||
<PeopleListSelection />
|
||||
{!showReviewForm.isOpen && (
|
||||
<Button colorScheme="brand" ml="auto" mb="2" onClick={showReviewForm.onOpen}>
|
||||
<Button colorScheme="primary" ml="auto" mb="2" onClick={showReviewForm.onOpen}>
|
||||
Write review
|
||||
</Button>
|
||||
)}
|
||||
|
@ -56,7 +56,7 @@ export default function RelayReviewForm({
|
||||
<Textarea {...register("content")} rows={5} placeholder="A short description of your experience with the relay" />
|
||||
<Flex gap="2" ml="auto">
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button type="submit" colorScheme="brand">
|
||||
<Button type="submit" colorScheme="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</Flex>
|
||||
|
@ -50,7 +50,7 @@ export default function SettingsView() {
|
||||
ml="auto"
|
||||
isLoading={form.formState.isLoading || form.formState.isValidating || form.formState.isSubmitting}
|
||||
isDisabled={!form.formState.isDirty}
|
||||
colorScheme="brand"
|
||||
colorScheme="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save Settings
|
||||
|
@ -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();
|
||||
@ -81,11 +81,11 @@ export default function ChatMessageForm({ stream }: { stream: ParsedStream }) {
|
||||
if (file) uploadImage(file);
|
||||
}}
|
||||
/>
|
||||
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
||||
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
|
||||
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 { memo, useRef } from "react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
|
||||
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";
|
||||
|
||||
function ChatCard({ stream }: { stream: ParsedStream }) {
|
||||
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);
|
122
src/views/tools/stream-moderation/index.tsx
Normal file
122
src/views/tools/stream-moderation/index.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
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: "users",
|
||||
splitPercentage: 40,
|
||||
},
|
||||
second: {
|
||||
direction: "row",
|
||||
first: "zaps",
|
||||
second: "chat",
|
||||
},
|
||||
splitPercentage: 33,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
97
src/views/tools/stream-moderation/users-card.tsx
Normal file
97
src/views/tools/stream-moderation/users-card.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { ReactNode, memo, useMemo, useState } 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 { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
import { useInterval } from "react-use";
|
||||
|
||||
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();
|
||||
|
||||
let buttons: ReactNode | null = null;
|
||||
if (isMuted) {
|
||||
if (expiration === Infinity) {
|
||||
buttons = <Button onClick={unmute}>Unban</Button>;
|
||||
} else {
|
||||
buttons = (
|
||||
<Button onClick={unmute}>
|
||||
Unmute (<Countdown time={expiration} />)
|
||||
</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 }: { 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 (
|
||||
<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);
|
18
src/views/tools/stream-moderation/video-card.tsx
Normal file
18
src/views/tools/stream-moderation/video-card.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { memo } from "react";
|
||||
|
||||
import { LiveVideoPlayer } from "../../../components/live-video-player";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
function LiveVideoCard({ stream }: { stream: ParsedStream }) {
|
||||
return (
|
||||
<LiveVideoPlayer
|
||||
stream={stream.streaming || stream.recording}
|
||||
autoPlay={stream.streaming ? true : undefined}
|
||||
poster={stream.image}
|
||||
maxH="50vh"
|
||||
muted
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LiveVideoCard);
|
31
src/views/tools/stream-moderation/zaps-card.tsx
Normal file
31
src/views/tools/stream-moderation/zaps-card.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
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 ZapMessageMemo from "../../streams/stream/stream-chat/zap-message";
|
||||
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||
|
||||
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) => {
|
||||
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">
|
||||
{zapMessages.map((event) => (
|
||||
<ZapMessageMemo key={event.id} zap={event} stream={stream} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ZapsCard);
|
@ -42,7 +42,7 @@ export default function Header({
|
||||
aria-label="Edit profile"
|
||||
title="Edit profile"
|
||||
size="sm"
|
||||
colorScheme="brand"
|
||||
colorScheme="primary"
|
||||
onClick={() => navigate("/profile")}
|
||||
/>
|
||||
)}
|
||||
|
@ -5,7 +5,7 @@ import { useCopyToClipboard } from "react-use";
|
||||
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import {
|
||||
ChatIcon,
|
||||
MessagesIcon,
|
||||
ClipboardIcon,
|
||||
CodeIcon,
|
||||
ExternalLinkIcon,
|
||||
@ -63,7 +63,7 @@ export const UserProfileMenu = ({
|
||||
{isMuted ? "Unmute User" : "Mute User"}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem icon={<ChatIcon fontSize="1.5em" />} as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}`}>
|
||||
<MenuItem icon={<MessagesIcon fontSize="1.5em" />} as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}`}>
|
||||
Direct messages
|
||||
</MenuItem>
|
||||
<MenuItem icon={<SpyIcon fontSize="1.5em" />} onClick={() => loginAsUser()}>
|
||||
|
@ -115,7 +115,7 @@ const UserView = () => {
|
||||
isLazy
|
||||
index={activeTab}
|
||||
onChange={(v) => navigate(tabs[v].path, { replace: true })}
|
||||
colorScheme="brand"
|
||||
colorScheme="primary"
|
||||
h="full"
|
||||
>
|
||||
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
||||
|
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