mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 04:39:19 +02:00
add vote buttons to communities
This commit is contained in:
parent
701a309a62
commit
5d66750768
5
.changeset/hungry-taxis-turn.md
Normal file
5
.changeset/hungry-taxis-turn.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add vote buttons to community view
|
5
.changeset/rotten-socks-sneeze.md
Normal file
5
.changeset/rotten-socks-sneeze.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve community view on mobile
|
10
src/app.tsx
10
src/app.tsx
@ -58,10 +58,11 @@ import BadgesBrowseView from "./views/badges/browse";
|
|||||||
import BadgeDetailsView from "./views/badges/badge-details";
|
import BadgeDetailsView from "./views/badges/badge-details";
|
||||||
|
|
||||||
import CommunitiesHomeView from "./views/communities";
|
import CommunitiesHomeView from "./views/communities";
|
||||||
|
import CommunitiesExploreView from "./views/communities/explore";
|
||||||
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 CommunityPendingView from "./views/community/views/pending";
|
import CommunityPendingView from "./views/community/views/pending";
|
||||||
import CommunityNewView from "./views/community/views/new";
|
import CommunityNewestView from "./views/community/views/newest";
|
||||||
|
|
||||||
import RelaysView from "./views/relays";
|
import RelaysView from "./views/relays";
|
||||||
import RelayView from "./views/relays/relay";
|
import RelayView from "./views/relays/relay";
|
||||||
@ -236,7 +237,10 @@ const router = createHashRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "communities",
|
path: "communities",
|
||||||
element: <CommunitiesHomeView />,
|
children: [
|
||||||
|
{ path: "", element: <CommunitiesHomeView /> },
|
||||||
|
{ path: "explore", element: <CommunitiesExploreView /> },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "c/:community",
|
path: "c/:community",
|
||||||
@ -246,7 +250,7 @@ const router = createHashRouter([
|
|||||||
path: ":pubkey",
|
path: ":pubkey",
|
||||||
element: <CommunityView />,
|
element: <CommunityView />,
|
||||||
children: [
|
children: [
|
||||||
{ path: "", element: <CommunityNewView /> },
|
{ path: "", element: <CommunityNewestView /> },
|
||||||
{ path: "pending", element: <CommunityPendingView /> },
|
{ path: "pending", element: <CommunityPendingView /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -1,47 +1,44 @@
|
|||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { Box, Card, CardProps, Center, Flex, Heading, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
import { Card, CardFooter, CardHeader, CardProps, Heading, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities";
|
import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities";
|
||||||
import CommunityDescription from "../../../views/communities/components/community-description";
|
|
||||||
|
|
||||||
export default function EmbeddedCommunity({
|
export default function EmbeddedCommunity({
|
||||||
community,
|
community,
|
||||||
...props
|
...props
|
||||||
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
||||||
const image = getCommunityImage(community);
|
|
||||||
const name = getCommunityName(community);
|
const name = getCommunityName(community);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card as={LinkBox} variant="outline" gap="2" overflow="hidden" {...props}>
|
<Card
|
||||||
{image ? (
|
as={LinkBox}
|
||||||
<Box
|
variant="outline"
|
||||||
backgroundImage={getCommunityImage(community)}
|
gap="2"
|
||||||
backgroundRepeat="no-repeat"
|
overflow="hidden"
|
||||||
backgroundSize="contain"
|
borderRadius="xl"
|
||||||
backgroundPosition="center"
|
backgroundImage={getCommunityImage(community)}
|
||||||
aspectRatio={3 / 1}
|
backgroundRepeat="no-repeat"
|
||||||
/>
|
backgroundSize="cover"
|
||||||
) : (
|
backgroundPosition="center"
|
||||||
<Center aspectRatio={4 / 1} fontWeight="bold" fontSize="2xl">
|
textShadow="2px 2px var(--chakra-blur-sm) var(--chakra-colors-blackAlpha-800)"
|
||||||
{name}
|
{...props}
|
||||||
</Center>
|
>
|
||||||
)}
|
<CardHeader pb="0">
|
||||||
<Flex direction="column" flex={1} px="2" pb="2">
|
<Heading size="lg">
|
||||||
<Flex wrap="wrap" gap="2" alignItems="center">
|
<LinkOverlay as={RouterLink} to={`/c/${encodeURIComponent(name)}/${nip19.npubEncode(community.pubkey)}`}>
|
||||||
<Heading size="lg">
|
{name}
|
||||||
<LinkOverlay as={RouterLink} to={`/c/${encodeURIComponent(name)}/${nip19.npubEncode(community.pubkey)}`}>
|
</LinkOverlay>
|
||||||
{name}
|
</Heading>
|
||||||
</LinkOverlay>
|
</CardHeader>
|
||||||
</Heading>
|
<CardFooter display="flex" alignItems="center" gap="2" pt="0">
|
||||||
<Text>Created by:</Text>
|
<UserAvatarLink pubkey={community.pubkey} size="sm" />
|
||||||
<UserAvatarLink pubkey={community.pubkey} size="xs" /> <UserLink pubkey={community.pubkey} />
|
<Text>by</Text>
|
||||||
</Flex>
|
<UserLink pubkey={community.pubkey} />
|
||||||
<CommunityDescription community={community} maxLength={128} flex={1} />
|
</CardFooter>
|
||||||
</Flex>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ export default function NavItems() {
|
|||||||
else if (location.pathname.startsWith("/relays")) active = "relays";
|
else if (location.pathname.startsWith("/relays")) active = "relays";
|
||||||
else if (location.pathname.startsWith("/lists")) active = "lists";
|
else if (location.pathname.startsWith("/lists")) active = "lists";
|
||||||
else if (location.pathname.startsWith("/communities")) active = "communities";
|
else if (location.pathname.startsWith("/communities")) active = "communities";
|
||||||
|
else if (location.pathname.startsWith("/c/")) active = "communities";
|
||||||
else if (location.pathname.startsWith("/goals")) active = "goals";
|
else if (location.pathname.startsWith("/goals")) active = "goals";
|
||||||
else if (location.pathname.startsWith("/badges")) active = "badges";
|
else if (location.pathname.startsWith("/badges")) active = "badges";
|
||||||
else if (location.pathname.startsWith("/emojis")) active = "emojis";
|
else if (location.pathname.startsWith("/emojis")) active = "emojis";
|
||||||
|
@ -18,7 +18,7 @@ import { NostrEvent, isATag } from "../../types/nostr-event";
|
|||||||
import { UserAvatarLink } from "../user-avatar-link";
|
import { UserAvatarLink } from "../user-avatar-link";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import { NoteMenu } from "./note-menu";
|
import NoteMenu from "./note-menu";
|
||||||
import { EventRelays } from "./note-relays";
|
import { EventRelays } from "./note-relays";
|
||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
|
||||||
|
@ -29,7 +29,7 @@ import NostrPublishAction from "../../classes/nostr-publish-action";
|
|||||||
import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
|
import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
|
||||||
import { useMuteModalContext } from "../../providers/mute-modal-provider";
|
import { useMuteModalContext } from "../../providers/mute-modal-provider";
|
||||||
|
|
||||||
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) => {
|
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const infoModal = useDisclosure();
|
const infoModal = useDisclosure();
|
||||||
const reactionsModal = useDisclosure();
|
const reactionsModal = useDisclosure();
|
||||||
@ -101,4 +101,4 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { Kind, validateEvent } from "nostr-tools";
|
|||||||
|
|
||||||
import { isETag, NostrEvent } from "../../../types/nostr-event";
|
import { isETag, NostrEvent } from "../../../types/nostr-event";
|
||||||
import { Note } from "../../note";
|
import { Note } from "../../note";
|
||||||
import { NoteMenu } from "../../note/note-menu";
|
import NoteMenu from "../../note/note-menu";
|
||||||
import { UserAvatar } from "../../user-avatar";
|
import { UserAvatar } from "../../user-avatar";
|
||||||
import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
|
import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
|
||||||
import { UserLink } from "../../user-link";
|
import { UserLink } from "../../user-link";
|
||||||
|
@ -25,6 +25,9 @@ export function getCommunityImage(community: NostrEvent) {
|
|||||||
export function getCommunityDescription(community: NostrEvent) {
|
export function getCommunityDescription(community: NostrEvent) {
|
||||||
return community.tags.find((t) => t[0] === "description")?.[1];
|
return community.tags.find((t) => t[0] === "description")?.[1];
|
||||||
}
|
}
|
||||||
|
export function getCommunityRules(community: NostrEvent) {
|
||||||
|
return community.tags.find((t) => t[0] === "rules")?.[1];
|
||||||
|
}
|
||||||
|
|
||||||
export function getApprovedEmbeddedNote(approval: NostrEvent) {
|
export function getApprovedEmbeddedNote(approval: NostrEvent) {
|
||||||
if (!approval.content) return null;
|
if (!approval.content) return null;
|
||||||
|
@ -1,27 +1,39 @@
|
|||||||
import stringify from "json-stringify-deterministic";
|
import stringify from "json-stringify-deterministic";
|
||||||
|
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
import SuperMap from "../classes/super-map";
|
import SuperMap from "../classes/super-map";
|
||||||
import { NostrRequestFilter } from "../types/nostr-query";
|
import { NostrRequestFilter } from "../types/nostr-query";
|
||||||
import NostrRequest from "../classes/nostr-request";
|
import NostrRequest from "../classes/nostr-request";
|
||||||
|
import relayPoolService from "./relay-pool";
|
||||||
|
|
||||||
// TODO: move this to settings
|
// TODO: move this to settings
|
||||||
const COUNT_RELAY = "wss://relay.nostr.band";
|
const COUNT_RELAY = "wss://relay.nostr.band";
|
||||||
|
|
||||||
|
const RATE_LIMIT = 10 / 1000;
|
||||||
|
|
||||||
class EventCountService {
|
class EventCountService {
|
||||||
subjects = new SuperMap<string, Subject<number>>(() => new Subject<number>());
|
subjects = new SuperMap<string, Subject<number>>(() => new Subject<number>());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
window.setInterval(this.makeRequest.bind(this), RATE_LIMIT);
|
||||||
|
relayPoolService.addClaim(COUNT_RELAY, this);
|
||||||
|
}
|
||||||
|
|
||||||
stringifyFilter(filter: NostrRequestFilter) {
|
stringifyFilter(filter: NostrRequestFilter) {
|
||||||
return stringify(filter);
|
return stringify(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private queue: NostrRequestFilter[] = [];
|
||||||
|
private queueKeys = new Set<string>();
|
||||||
requestCount(filter: NostrRequestFilter, alwaysRequest = false) {
|
requestCount(filter: NostrRequestFilter, alwaysRequest = false) {
|
||||||
const key = this.stringifyFilter(filter);
|
const key = this.stringifyFilter(filter);
|
||||||
const sub = this.subjects.get(key);
|
const sub = this.subjects.get(key);
|
||||||
|
|
||||||
if (sub.value === undefined || alwaysRequest) {
|
if (sub.value === undefined || alwaysRequest) {
|
||||||
const request = new NostrRequest([COUNT_RELAY]);
|
if (!this.queueKeys.has(key)) {
|
||||||
request.onCount.subscribe((c) => sub.next(c.count));
|
this.queue.push(filter);
|
||||||
request.start(filter, "COUNT");
|
this.queueKeys.add(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sub;
|
return sub;
|
||||||
@ -32,6 +44,19 @@ class EventCountService {
|
|||||||
const sub = this.subjects.get(key);
|
const sub = this.subjects.get(key);
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeRequest() {
|
||||||
|
const filter = this.queue.pop();
|
||||||
|
if (!filter) return;
|
||||||
|
|
||||||
|
const key = this.stringifyFilter(filter);
|
||||||
|
this.queueKeys.delete(key);
|
||||||
|
|
||||||
|
const sub = this.subjects.get(key);
|
||||||
|
const request = new NostrRequest([COUNT_RELAY]);
|
||||||
|
request.onCount.subscribe((c) => sub.next(c.count));
|
||||||
|
request.start(filter, "COUNT");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventCountService = new EventCountService();
|
const eventCountService = new EventCountService();
|
||||||
|
@ -1,55 +1,67 @@
|
|||||||
import { memo, useRef } from "react";
|
import { memo, useRef } from "react";
|
||||||
import { nip19 } from "nostr-tools";
|
import { Kind, nip19 } from "nostr-tools";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { Box, Card, CardProps, Center, Flex, Heading, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
import {
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardProps,
|
||||||
|
Heading,
|
||||||
|
LinkBox,
|
||||||
|
LinkOverlay,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
|
||||||
import { UserLink } from "../../../components/user-link";
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
import { getEventUID } from "../../../helpers/nostr/events";
|
import { getEventUID } from "../../../helpers/nostr/events";
|
||||||
import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities";
|
import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities";
|
||||||
import CommunityDescription from "./community-description";
|
import CommunityDescription from "./community-description";
|
||||||
import CommunityModList from "./community-mod-list";
|
import useCountCommunityPosts from "../hooks/use-count-community-post";
|
||||||
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
|
import { UserLink } from "../../../components/user-link";
|
||||||
|
|
||||||
function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
||||||
// if there is a parent intersection observer, register this card
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useRegisterIntersectionEntity(ref, getEventUID(community));
|
useRegisterIntersectionEntity(ref, getEventUID(community));
|
||||||
|
|
||||||
const name = getCommunityName(community);
|
const name = getCommunityName(community);
|
||||||
const image = getCommunityImage(community);
|
|
||||||
|
// NOTE: disabled because nostr.band has a rate limit
|
||||||
|
// const notesInLastMonth = useCountCommunityPosts(community);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card as={LinkBox} ref={ref} variant="outline" gap="2" overflow="hidden" {...props}>
|
<Card
|
||||||
{image ? (
|
as={LinkBox}
|
||||||
<Box
|
ref={ref}
|
||||||
backgroundImage={getCommunityImage(community)}
|
variant="outline"
|
||||||
backgroundRepeat="no-repeat"
|
gap="2"
|
||||||
backgroundSize="cover"
|
overflow="hidden"
|
||||||
backgroundPosition="center"
|
borderRadius="xl"
|
||||||
aspectRatio={3 / 1}
|
backgroundImage={getCommunityImage(community)}
|
||||||
/>
|
backgroundRepeat="no-repeat"
|
||||||
) : (
|
backgroundSize="cover"
|
||||||
<Center aspectRatio={3 / 1} fontWeight="bold" fontSize="2xl">
|
backgroundPosition="center"
|
||||||
{name}
|
textShadow="2px 2px var(--chakra-blur-sm) var(--chakra-colors-blackAlpha-800)"
|
||||||
</Center>
|
{...props}
|
||||||
)}
|
>
|
||||||
<Flex direction="column" flex={1} px="2" pb="2">
|
<CardHeader pb="0">
|
||||||
<Flex wrap="wrap" gap="2" alignItems="center">
|
<Heading size="lg">
|
||||||
<Heading size="lg">
|
<LinkOverlay as={RouterLink} to={`/c/${encodeURIComponent(name)}/${nip19.npubEncode(community.pubkey)}`}>
|
||||||
<LinkOverlay as={RouterLink} to={`/c/${encodeURIComponent(name)}/${nip19.npubEncode(community.pubkey)}`}>
|
{name}
|
||||||
{name}
|
</LinkOverlay>
|
||||||
</LinkOverlay>
|
</Heading>
|
||||||
</Heading>
|
</CardHeader>
|
||||||
<Text>Created by:</Text>
|
<CardBody>
|
||||||
<UserAvatarLink pubkey={community.pubkey} size="xs" /> <UserLink pubkey={community.pubkey} />
|
|
||||||
</Flex>
|
|
||||||
<CommunityDescription community={community} maxLength={128} flex={1} />
|
<CommunityDescription community={community} maxLength={128} flex={1} />
|
||||||
<Flex gap="2">
|
</CardBody>
|
||||||
<CommunityModList community={community} ml="auto" size="xs" />
|
<CardFooter display="flex" alignItems="center" gap="2" pt="0">
|
||||||
</Flex>
|
<UserAvatarLink pubkey={community.pubkey} size="sm" />
|
||||||
</Flex>
|
<Text>by</Text>
|
||||||
|
<UserLink pubkey={community.pubkey} />
|
||||||
|
{/* {notesInLastMonth !== undefined && <Text ml="auto">{notesInLastMonth} Posts in the past month</Text>} */}
|
||||||
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
69
src/views/communities/explore.tsx
Normal file
69
src/views/communities/explore.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
||||||
|
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||||
|
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||||
|
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||||
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
import CommunityCard from "./components/community-card";
|
||||||
|
import { getEventUID } from "../../helpers/nostr/events";
|
||||||
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
||||||
|
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
||||||
|
import { COMMUNITY_DEFINITION_KIND, validateCommunity } from "../../helpers/nostr/communities";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import { NostrQuery } from "../../types/nostr-query";
|
||||||
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
|
|
||||||
|
function CommunitiesExplorePage() {
|
||||||
|
const { filter, listId } = usePeopleListContext();
|
||||||
|
const { relays } = useRelaySelectionContext();
|
||||||
|
|
||||||
|
const eventFilter = useCallback((event: NostrEvent) => {
|
||||||
|
return validateCommunity(event);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const query = useMemo(() => {
|
||||||
|
const base: NostrQuery = { kinds: [COMMUNITY_DEFINITION_KIND] };
|
||||||
|
if (filter?.authors) {
|
||||||
|
base.authors = filter.authors;
|
||||||
|
base["#p"] = filter.authors;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
|
const timeline = useTimelineLoader(`${listId}-browse-communities`, relays, query, { enabled: !!filter, eventFilter });
|
||||||
|
|
||||||
|
const communities = useSubject(timeline.timeline);
|
||||||
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntersectionObserverProvider callback={callback}>
|
||||||
|
<VerticalPageLayout>
|
||||||
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
|
<PeopleListSelection />
|
||||||
|
<RelaySelectionButton />
|
||||||
|
</Flex>
|
||||||
|
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
||||||
|
{communities.map((event) => (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<CommunityCard key={getEventUID(event)} community={event} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</VerticalPageLayout>
|
||||||
|
</IntersectionObserverProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExploreCommunitiesView() {
|
||||||
|
return (
|
||||||
|
<PeopleListProvider initList="global">
|
||||||
|
<RelaySelectionProvider>
|
||||||
|
<CommunitiesExplorePage />
|
||||||
|
</RelaySelectionProvider>
|
||||||
|
</PeopleListProvider>
|
||||||
|
);
|
||||||
|
}
|
13
src/views/communities/hooks/use-count-community-post.ts
Normal file
13
src/views/communities/hooks/use-count-community-post.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
|
import useEventCount from "../../../hooks/use-event-count";
|
||||||
|
import { getEventCoordinate } from "../../../helpers/nostr/events";
|
||||||
|
|
||||||
|
export default function useCountCommunityPosts(
|
||||||
|
community: NostrEvent,
|
||||||
|
since: number = dayjs().subtract(1, "month").unix(),
|
||||||
|
) {
|
||||||
|
return useEventCount({ "#a": [getEventCoordinate(community)], kinds: [Kind.Text], since });
|
||||||
|
}
|
@ -1,69 +1,48 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { Button, Center, Flex, Heading, Link, SimpleGrid, Text } from "@chakra-ui/react";
|
||||||
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
|
|
||||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
|
||||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
|
||||||
import useSubject from "../../hooks/use-subject";
|
|
||||||
import CommunityCard from "./components/community-card";
|
|
||||||
import { getEventUID } from "../../helpers/nostr/events";
|
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
|
|
||||||
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
|
|
||||||
import { COMMUNITY_DEFINITION_KIND, validateCommunity } from "../../helpers/nostr/communities";
|
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
|
||||||
import { NostrQuery } from "../../types/nostr-query";
|
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
|
import useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list";
|
||||||
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { EmbedEventPointer } from "../../components/embed-event";
|
||||||
|
|
||||||
function CommunitiesHomePage() {
|
function CommunitiesHomePage() {
|
||||||
const { filter, listId } = usePeopleListContext();
|
const account = useCurrentAccount()!;
|
||||||
const { relays } = useRelaySelectionContext();
|
const { pointers: communities } = useSubscribedCommunitiesList(account.pubkey);
|
||||||
|
|
||||||
const eventFilter = useCallback((event: NostrEvent) => {
|
|
||||||
return validateCommunity(event);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const query = useMemo(() => {
|
|
||||||
const base: NostrQuery = { kinds: [COMMUNITY_DEFINITION_KIND] };
|
|
||||||
if (filter?.authors) {
|
|
||||||
base.authors = filter.authors;
|
|
||||||
base["#p"] = filter.authors;
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}, [filter]);
|
|
||||||
|
|
||||||
const timeline = useTimelineLoader(`${listId}-browse-communities`, relays, query, { enabled: !!filter, eventFilter });
|
|
||||||
|
|
||||||
const communities = useSubject(timeline.timeline);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<VerticalPageLayout>
|
||||||
<VerticalPageLayout>
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<Button as={RouterLink} to="/communities/explore">
|
||||||
<PeopleListSelection />
|
Explore Communities
|
||||||
<RelaySelectionButton />
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{communities.length > 0 ? (
|
||||||
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
||||||
{communities.map((event) => (
|
{communities.map((pointer) => (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<CommunityCard key={getEventUID(event)} community={event} />
|
<EmbedEventPointer pointer={{ type: "naddr", data: pointer }} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</VerticalPageLayout>
|
) : (
|
||||||
</IntersectionObserverProvider>
|
<Center aspectRatio={3 / 4} flexDirection="column" gap="4">
|
||||||
|
<Heading size="md">No communities :(</Heading>
|
||||||
|
<Text>
|
||||||
|
go find a cool one to join.{" "}
|
||||||
|
<Link as={RouterLink} to="/communities/explore" color="blue.500">
|
||||||
|
Explore
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CommunitiesHomeView() {
|
export default function CommunitiesHomeView() {
|
||||||
return (
|
const account = useCurrentAccount();
|
||||||
<PeopleListProvider initList="global">
|
return account ? <CommunitiesHomePage /> : <Navigate to="/communities/explore" />;
|
||||||
<RelaySelectionProvider>
|
|
||||||
<CommunitiesHomePage />
|
|
||||||
</RelaySelectionProvider>
|
|
||||||
</PeopleListProvider>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -21,46 +21,9 @@ import { RelayIconStack } from "../../components/relay-icon-stack";
|
|||||||
import TrendUp01 from "../../components/icons/trend-up-01";
|
import TrendUp01 from "../../components/icons/trend-up-01";
|
||||||
import Clock from "../../components/icons/clock";
|
import Clock from "../../components/icons/clock";
|
||||||
import Hourglass03 from "../../components/icons/hourglass-03";
|
import Hourglass03 from "../../components/icons/hourglass-03";
|
||||||
|
import VerticalCommunityDetails from "./components/vertical-community-details";
|
||||||
function CommunityDetails({ community }: { community: NostrEvent }) {
|
import { useBreakpointValue } from "../../providers/breakpoint-provider";
|
||||||
const communityRelays = getCommunityRelays(community);
|
import HorizontalCommunityDetails from "./components/horizonal-community-details";
|
||||||
const mods = getCommunityMods(community);
|
|
||||||
const description = getCommunityDescription(community);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card p="4" w="xs" flexShrink={0}>
|
|
||||||
{description && (
|
|
||||||
<>
|
|
||||||
<Heading size="sm" mb="2">
|
|
||||||
Description:
|
|
||||||
</Heading>
|
|
||||||
<CommunityDescription community={community} maxLength={256} showExpand />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Heading size="sm" mt="4" mb="2">
|
|
||||||
Moderators:
|
|
||||||
</Heading>
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
{mods.map((pubkey) => (
|
|
||||||
<Flex gap="2">
|
|
||||||
<UserAvatarLink pubkey={pubkey} size="xs" />
|
|
||||||
<UserLink pubkey={pubkey} />
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
{communityRelays.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Heading size="sm" mt="4" mb="2">
|
|
||||||
Relays:
|
|
||||||
</Heading>
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
<RelayIconStack relays={communityRelays} />
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommunityPath(community: NostrEvent) {
|
function getCommunityPath(community: NostrEvent) {
|
||||||
return `/c/${encodeURIComponent(getCommunityName(community))}/${nip19.npubEncode(community.pubkey)}`;
|
return `/c/${encodeURIComponent(getCommunityName(community))}/${nip19.npubEncode(community.pubkey)}`;
|
||||||
@ -70,6 +33,8 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
|||||||
const image = getCommunityImage(community);
|
const image = getCommunityImage(community);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
const verticalLayout = useBreakpointValue({ base: true, xl: false });
|
||||||
|
|
||||||
const communityRelays = getCommunityRelays(community);
|
const communityRelays = getCommunityRelays(community);
|
||||||
|
|
||||||
let active = "new";
|
let active = "new";
|
||||||
@ -78,27 +43,31 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
|||||||
return (
|
return (
|
||||||
<AdditionalRelayProvider relays={communityRelays}>
|
<AdditionalRelayProvider relays={communityRelays}>
|
||||||
<VerticalPageLayout pt={image && "0"}>
|
<VerticalPageLayout pt={image && "0"}>
|
||||||
{image && (
|
<Flex
|
||||||
<Box
|
backgroundImage={getCommunityImage(community)}
|
||||||
backgroundImage={getCommunityImage(community)}
|
backgroundRepeat="no-repeat"
|
||||||
backgroundRepeat="no-repeat"
|
backgroundSize="cover"
|
||||||
backgroundSize="cover"
|
backgroundPosition="center"
|
||||||
backgroundPosition="center"
|
aspectRatio={3 / 1}
|
||||||
aspectRatio={3 / 1}
|
backgroundColor="rgba(0,0,0,0.2)"
|
||||||
backgroundColor="rgba(0,0,0,0.2)"
|
p="4"
|
||||||
/>
|
gap="4"
|
||||||
)}
|
direction="column"
|
||||||
<Flex wrap="wrap" gap="2" alignItems="center">
|
justifyContent="flex-end"
|
||||||
<Heading size="lg">{getCommunityName(community)}</Heading>
|
textShadow="2px 2px var(--chakra-blur-sm) var(--chakra-colors-blackAlpha-800)"
|
||||||
<Text>Created by:</Text>
|
>
|
||||||
<Flex gap="2">
|
<Heading>{getCommunityName(community)}</Heading>
|
||||||
<UserAvatarLink pubkey={community.pubkey} size="xs" /> <UserLink pubkey={community.pubkey} />
|
<Flex gap="2" alignItems="center">
|
||||||
|
<UserAvatarLink pubkey={community.pubkey} size="sm" />
|
||||||
|
<Text>by</Text>
|
||||||
|
<UserLink pubkey={community.pubkey} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<CommunityJoinButton community={community} ml="auto" />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex gap="4" alignItems="flex-start">
|
{verticalLayout && <HorizontalCommunityDetails community={community} w="full" flexShrink={0} />}
|
||||||
<Flex direction="column" gap="4" flex={1}>
|
|
||||||
|
<Flex gap="4" alignItems="flex-start" overflow="hidden">
|
||||||
|
<Flex direction="column" gap="4" flex={1} overflow="hidden">
|
||||||
<ButtonGroup size="sm">
|
<ButtonGroup size="sm">
|
||||||
<Button leftIcon={<TrendUp01 />} isDisabled>
|
<Button leftIcon={<TrendUp01 />} isDisabled>
|
||||||
Trending
|
Trending
|
||||||
@ -124,7 +93,7 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
|||||||
<Outlet context={{ community }} />
|
<Outlet context={{ community }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<CommunityDetails community={community} />
|
{!verticalLayout && <VerticalCommunityDetails community={community} w="full" maxW="xs" flexShrink={0} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
</AdditionalRelayProvider>
|
</AdditionalRelayProvider>
|
||||||
|
41
src/views/community/components/community-menu.tsx
Normal file
41
src/views/community/components/community-menu.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||||
|
import { useCopyToClipboard } from "react-use";
|
||||||
|
|
||||||
|
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||||
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
|
import { CodeIcon, ExternalLinkIcon, RepostIcon } from "../../../components/icons";
|
||||||
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
|
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||||
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
|
|
||||||
|
export default function CommunityMenu({
|
||||||
|
community,
|
||||||
|
...props
|
||||||
|
}: Omit<MenuIconButtonProps, "children"> & { community: NostrEvent }) {
|
||||||
|
const debugModal = useDisclosure();
|
||||||
|
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||||
|
|
||||||
|
const address = getSharableEventAddress(community);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CustomMenuIconButton {...props}>
|
||||||
|
{address && (
|
||||||
|
<MenuItem onClick={() => window.open(buildAppSelectUrl(address), "_blank")} icon={<ExternalLinkIcon />}>
|
||||||
|
View in app...
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem onClick={() => copyToClipboard("nostr:" + address)} icon={<RepostIcon />}>
|
||||||
|
Copy Share Link
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
||||||
|
View Raw
|
||||||
|
</MenuItem>
|
||||||
|
</CustomMenuIconButton>
|
||||||
|
|
||||||
|
{debugModal.isOpen && (
|
||||||
|
<NoteDebugModal event={community} isOpen={debugModal.isOpen} onClose={debugModal.onClose} size="6xl" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardProps,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
SimpleGrid,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
getCommunityDescription,
|
||||||
|
getCommunityMods,
|
||||||
|
getCommunityRelays,
|
||||||
|
getCommunityRules,
|
||||||
|
} from "../../../helpers/nostr/communities";
|
||||||
|
import CommunityDescription from "../../communities/components/community-description";
|
||||||
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
|
import { UserLink } from "../../../components/user-link";
|
||||||
|
import { RelayIconStack } from "../../../components/relay-icon-stack";
|
||||||
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
|
import CommunityJoinButton from "../../communities/components/community-subscribe-button";
|
||||||
|
import CommunityMenu from "./community-menu";
|
||||||
|
|
||||||
|
export default function HorizontalCommunityDetails({
|
||||||
|
community,
|
||||||
|
...props
|
||||||
|
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
||||||
|
const communityRelays = getCommunityRelays(community);
|
||||||
|
const mods = getCommunityMods(community);
|
||||||
|
const description = getCommunityDescription(community);
|
||||||
|
const rules = getCommunityRules(community);
|
||||||
|
|
||||||
|
const more = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card {...props}>
|
||||||
|
<CardBody>
|
||||||
|
<ButtonGroup float="right">
|
||||||
|
<CommunityJoinButton community={community} />
|
||||||
|
<CommunityMenu community={community} aria-label="More" />
|
||||||
|
</ButtonGroup>
|
||||||
|
{description && (
|
||||||
|
<>
|
||||||
|
<Heading size="sm" mb="2">
|
||||||
|
Description
|
||||||
|
</Heading>
|
||||||
|
<CommunityDescription community={community} mb="2" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{more.isOpen ? (
|
||||||
|
<SimpleGrid spacing="4" columns={2}>
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" mb="2">
|
||||||
|
Mods
|
||||||
|
</Heading>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
{mods.map((pubkey) => (
|
||||||
|
<Flex gap="2">
|
||||||
|
<UserAvatarLink pubkey={pubkey} size="xs" />
|
||||||
|
<UserLink pubkey={pubkey} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
{rules && (
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" mb="2">
|
||||||
|
Rules
|
||||||
|
</Heading>
|
||||||
|
<Text whiteSpace="pre-wrap">{rules}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{communityRelays.length > 0 && (
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" mt="4" mb="2">
|
||||||
|
Relays
|
||||||
|
</Heading>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<RelayIconStack relays={communityRelays} />
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</SimpleGrid>
|
||||||
|
) : (
|
||||||
|
<Button variant="link" onClick={more.onOpen} w="full">
|
||||||
|
Show more
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
import { Box, ButtonGroup, Card, CardProps, Flex, Heading, Text } from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
getCommunityDescription,
|
||||||
|
getCommunityMods,
|
||||||
|
getCommunityRelays,
|
||||||
|
getCommunityRules,
|
||||||
|
} from "../../../helpers/nostr/communities";
|
||||||
|
import CommunityDescription from "../../communities/components/community-description";
|
||||||
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
|
import { UserLink } from "../../../components/user-link";
|
||||||
|
import { RelayIconStack } from "../../../components/relay-icon-stack";
|
||||||
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
|
import CommunityJoinButton from "../../communities/components/community-subscribe-button";
|
||||||
|
import CommunityMenu from "./community-menu";
|
||||||
|
|
||||||
|
export default function VerticalCommunityDetails({
|
||||||
|
community,
|
||||||
|
...props
|
||||||
|
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
||||||
|
const communityRelays = getCommunityRelays(community);
|
||||||
|
const mods = getCommunityMods(community);
|
||||||
|
const description = getCommunityDescription(community);
|
||||||
|
const rules = getCommunityRules(community);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card p="4" {...props}>
|
||||||
|
{description && (
|
||||||
|
<>
|
||||||
|
<Heading size="sm" mb="2">
|
||||||
|
About
|
||||||
|
</Heading>
|
||||||
|
<CommunityDescription community={community} maxLength={256} showExpand />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ButtonGroup w="full" my="2">
|
||||||
|
<CommunityJoinButton community={community} flex={1} />
|
||||||
|
<CommunityMenu community={community} aria-label="More" />
|
||||||
|
</ButtonGroup>
|
||||||
|
<Heading size="sm" mt="4" mb="2">
|
||||||
|
Mods
|
||||||
|
</Heading>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
{mods.map((pubkey) => (
|
||||||
|
<Flex gap="2">
|
||||||
|
<UserAvatarLink pubkey={pubkey} size="xs" />
|
||||||
|
<UserLink pubkey={pubkey} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
{rules && (
|
||||||
|
<>
|
||||||
|
<Heading size="sm" mt="4" mb="2">
|
||||||
|
Rules
|
||||||
|
</Heading>
|
||||||
|
<Text whiteSpace="pre-wrap">{rules}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{communityRelays.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Heading size="sm" mt="4" mb="2">
|
||||||
|
Relays
|
||||||
|
</Heading>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<RelayIconStack relays={communityRelays} />
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { Navigate, useParams } from "react-router-dom";
|
||||||
|
import { Heading, SimpleGrid } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { COMMUNITY_DEFINITION_KIND, validateCommunity } from "../../helpers/nostr/communities";
|
import { COMMUNITY_DEFINITION_KIND, validateCommunity } from "../../helpers/nostr/communities";
|
||||||
@ -11,11 +12,17 @@ import IntersectionObserverProvider from "../../providers/intersection-observer"
|
|||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import CommunityCard from "../communities/components/community-card";
|
import CommunityCard from "../communities/components/community-card";
|
||||||
import { getEventUID } from "../../helpers/nostr/events";
|
import { getEventUID } from "../../helpers/nostr/events";
|
||||||
import { Divider, Heading, SimpleGrid } from "@chakra-ui/react";
|
import { safeDecode } from "../../helpers/nip19";
|
||||||
|
|
||||||
export default function CommunityFindByNameView() {
|
export default function CommunityFindByNameView() {
|
||||||
const { community } = useParams() as { community: string };
|
const { community } = useParams() as { community: string };
|
||||||
|
|
||||||
|
// if community name is a naddr, redirect
|
||||||
|
const decoded = safeDecode(community);
|
||||||
|
if (decoded?.type === "naddr" && decoded.data.kind === COMMUNITY_DEFINITION_KIND) {
|
||||||
|
return <Navigate to={`/c/${decoded.data.identifier}/${decoded.data.pubkey}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const eventFilter = useCallback((event: NostrEvent) => {
|
const eventFilter = useCallback((event: NostrEvent) => {
|
||||||
return validateCommunity(event);
|
return validateCommunity(event);
|
||||||
@ -33,8 +40,7 @@ export default function CommunityFindByNameView() {
|
|||||||
return (
|
return (
|
||||||
<IntersectionObserverProvider callback={callback}>
|
<IntersectionObserverProvider callback={callback}>
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Heading>Select Community:</Heading>
|
<Heading>Select Community</Heading>
|
||||||
<Divider />
|
|
||||||
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
||||||
{communities.map((event) => (
|
{communities.map((event) => (
|
||||||
<CommunityCard key={getEventUID(event)} community={event} />
|
<CommunityCard key={getEventUID(event)} community={event} />
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import { Box } from "@chakra-ui/react";
|
|
||||||
import { useOutletContext } from "react-router-dom";
|
|
||||||
|
|
||||||
import { unique } from "../../../helpers/array";
|
|
||||||
import {
|
|
||||||
COMMUNITY_APPROVAL_KIND,
|
|
||||||
getApprovedEmbeddedNote,
|
|
||||||
getCommunityMods,
|
|
||||||
getCommunityRelays,
|
|
||||||
} from "../../../helpers/nostr/communities";
|
|
||||||
import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events";
|
|
||||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
|
||||||
import useSubject from "../../../hooks/use-subject";
|
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
|
||||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
|
||||||
import { NostrEvent, isETag } from "../../../types/nostr-event";
|
|
||||||
import { EmbedEvent } from "../../../components/embed-event";
|
|
||||||
import useSingleEvent from "../../../hooks/use-single-event";
|
|
||||||
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
|
||||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
|
||||||
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
|
||||||
|
|
||||||
function ApprovedEvent({ approval }: { approval: NostrEvent }) {
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
useRegisterIntersectionEntity(ref, getEventUID(approval));
|
|
||||||
|
|
||||||
const additionalRelays = useAdditionalRelayContext();
|
|
||||||
const embeddedEvent = getApprovedEmbeddedNote(approval);
|
|
||||||
const eventTag = approval.tags.find(isETag);
|
|
||||||
|
|
||||||
const loadEvent = useSingleEvent(
|
|
||||||
eventTag?.[1],
|
|
||||||
eventTag?.[2] ? [eventTag[2], ...additionalRelays] : additionalRelays,
|
|
||||||
);
|
|
||||||
const event = loadEvent || embeddedEvent;
|
|
||||||
if (!event) return;
|
|
||||||
return (
|
|
||||||
<Box ref={ref}>
|
|
||||||
<EmbedEvent event={event} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CommunityNewView() {
|
|
||||||
const { community } = useOutletContext() as { community: NostrEvent };
|
|
||||||
const mods = getCommunityMods(community);
|
|
||||||
|
|
||||||
const readRelays = useReadRelayUrls(getCommunityRelays(community));
|
|
||||||
const timeline = useTimelineLoader(`${getEventUID(community)}-approved-posts`, readRelays, {
|
|
||||||
authors: unique([community.pubkey, ...mods]),
|
|
||||||
kinds: [COMMUNITY_APPROVAL_KIND],
|
|
||||||
"#a": [getEventCoordinate(community)],
|
|
||||||
});
|
|
||||||
|
|
||||||
const approvals = useSubject(timeline.timeline);
|
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IntersectionObserverProvider callback={callback}>
|
|
||||||
{approvals.map((approval) => (
|
|
||||||
<ApprovedEvent key={getEventUID(approval)} approval={approval} />
|
|
||||||
))}
|
|
||||||
</IntersectionObserverProvider>
|
|
||||||
<TimelineActionAndStatus timeline={timeline} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
149
src/views/community/views/newest.tsx
Normal file
149
src/views/community/views/newest.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import { Box, Card, Flex, IconButton, Text, useToast } from "@chakra-ui/react";
|
||||||
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
|
||||||
|
import { unique } from "../../../helpers/array";
|
||||||
|
import {
|
||||||
|
COMMUNITY_APPROVAL_KIND,
|
||||||
|
getApprovedEmbeddedNote,
|
||||||
|
getCommunityMods,
|
||||||
|
getCommunityRelays,
|
||||||
|
} from "../../../helpers/nostr/communities";
|
||||||
|
import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events";
|
||||||
|
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||||
|
import useSubject from "../../../hooks/use-subject";
|
||||||
|
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
|
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||||
|
import { NostrEvent, isETag } from "../../../types/nostr-event";
|
||||||
|
import { EmbedEvent } from "../../../components/embed-event";
|
||||||
|
import useSingleEvent from "../../../hooks/use-single-event";
|
||||||
|
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
||||||
|
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||||
|
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
||||||
|
import { ChevronUpIcon } from "../../../components/icons";
|
||||||
|
import { ChevronDownIcon } from "@chakra-ui/icons";
|
||||||
|
import useEventReactions from "../../../hooks/use-event-reactions";
|
||||||
|
import { useMeasure, useStartTyping } from "react-use";
|
||||||
|
import { draftEventReaction } from "../../../helpers/nostr/reactions";
|
||||||
|
import eventReactionsService from "../../../services/event-reactions";
|
||||||
|
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||||
|
import clientRelaysService from "../../../services/client-relays";
|
||||||
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
|
|
||||||
|
function ApprovalVoteButtons({ event, community }: { event: NostrEvent; community: NostrEvent }) {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const reactions = useEventReactions(event.id);
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const voteReactions = useMemo(() => {
|
||||||
|
return reactions?.filter((r) => r.content === "+" || r.content === "-") ?? [];
|
||||||
|
}, [reactions]);
|
||||||
|
const vote = useMemo(() => {
|
||||||
|
return voteReactions.reduce((t, r) => {
|
||||||
|
if (r.content === "+") return t + 1;
|
||||||
|
else if (r.content === "-") return t - 1;
|
||||||
|
return t;
|
||||||
|
}, 0);
|
||||||
|
}, [voteReactions]);
|
||||||
|
|
||||||
|
const myVote = reactions?.find((e) => e.pubkey === account?.pubkey);
|
||||||
|
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const addVote = useCallback(
|
||||||
|
async (vote: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const draft = draftEventReaction(event, vote);
|
||||||
|
|
||||||
|
const signed = await requestSignature(draft);
|
||||||
|
if (signed) {
|
||||||
|
const writeRelays = clientRelaysService.getWriteUrls();
|
||||||
|
const communityRelays = getCommunityRelays(community);
|
||||||
|
new NostrPublishAction("Reaction", unique([...writeRelays, ...communityRelays]), signed);
|
||||||
|
eventReactionsService.handleEvent(signed);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
[event, community, requestSignature],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card direction="column" alignItems="center" borderRadius="lg">
|
||||||
|
<IconButton
|
||||||
|
aria-label="up vote"
|
||||||
|
title="up vote"
|
||||||
|
icon={<ChevronUpIcon boxSize={6} />}
|
||||||
|
size="sm"
|
||||||
|
variant={myVote?.content === "+" ? "solid" : "ghost"}
|
||||||
|
isLoading={loading}
|
||||||
|
onClick={() => addVote("+")}
|
||||||
|
isDisabled={!account || !!myVote}
|
||||||
|
colorScheme={myVote ? "primary" : "gray"}
|
||||||
|
/>
|
||||||
|
{voteReactions.length > 0 && <Text my="1">{vote}</Text>}
|
||||||
|
<IconButton
|
||||||
|
aria-label="down vote"
|
||||||
|
title="down vote"
|
||||||
|
icon={<ChevronDownIcon boxSize={6} />}
|
||||||
|
size="sm"
|
||||||
|
variant={myVote?.content === "-" ? "solid" : "ghost"}
|
||||||
|
isLoading={loading}
|
||||||
|
onClick={() => addVote("-")}
|
||||||
|
isDisabled={!account || !!myVote}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApprovedEvent({ approval, community }: { approval: NostrEvent; community: NostrEvent }) {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
useRegisterIntersectionEntity(ref, getEventUID(approval));
|
||||||
|
|
||||||
|
const additionalRelays = useAdditionalRelayContext();
|
||||||
|
const embeddedEvent = getApprovedEmbeddedNote(approval);
|
||||||
|
const eventTag = approval.tags.find(isETag);
|
||||||
|
|
||||||
|
const loadEvent = useSingleEvent(
|
||||||
|
eventTag?.[1],
|
||||||
|
eventTag?.[2] ? [eventTag[2], ...additionalRelays] : additionalRelays,
|
||||||
|
);
|
||||||
|
const event = loadEvent || embeddedEvent;
|
||||||
|
if (!event) return;
|
||||||
|
return (
|
||||||
|
<Flex ref={ref} gap="2" alignItems="flex-start" overflow="hidden">
|
||||||
|
<ApprovalVoteButtons event={event} community={community} />
|
||||||
|
<EmbedEvent event={event} flex={1} />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CommunityNewestView() {
|
||||||
|
const { community } = useOutletContext() as { community: NostrEvent };
|
||||||
|
const mods = getCommunityMods(community);
|
||||||
|
|
||||||
|
const readRelays = useReadRelayUrls(getCommunityRelays(community));
|
||||||
|
const timeline = useTimelineLoader(`${getEventUID(community)}-approved-posts`, readRelays, {
|
||||||
|
authors: unique([community.pubkey, ...mods]),
|
||||||
|
kinds: [COMMUNITY_APPROVAL_KIND],
|
||||||
|
"#a": [getEventCoordinate(community)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const approvals = useSubject(timeline.timeline);
|
||||||
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IntersectionObserverProvider callback={callback}>
|
||||||
|
{approvals.map((approval) => (
|
||||||
|
<ApprovedEvent key={getEventUID(approval)} approval={approval} community={community} />
|
||||||
|
))}
|
||||||
|
</IntersectionObserverProvider>
|
||||||
|
<TimelineActionAndStatus timeline={timeline} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -14,7 +14,7 @@ import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-
|
|||||||
import { TrustProvider } from "../../providers/trust";
|
import { TrustProvider } from "../../providers/trust";
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { NoteMenu } from "../../components/note/note-menu";
|
import NoteMenu from "../../components/note/note-menu";
|
||||||
import { EmbedEventPointer } from "../../components/embed-event";
|
import { EmbedEventPointer } from "../../components/embed-event";
|
||||||
import { embedEmoji } from "../../components/embed-types";
|
import { embedEmoji } from "../../components/embed-types";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user