small cleanup for communities

This commit is contained in:
hzrd149 2023-10-27 09:45:37 -05:00
parent 8a03bbb60e
commit 4b6dd2d4e4
10 changed files with 74 additions and 57 deletions

View File

@ -14,7 +14,7 @@ import {
Text,
useDisclosure,
} from "@chakra-ui/react";
import { NostrEvent, isATag } from "../../types/nostr-event";
import { NostrEvent } from "../../types/nostr-event";
import UserAvatarLink from "../user-avatar-link";
import { Link as RouterLink } from "react-router-dom";
@ -32,17 +32,16 @@ import { QuoteRepostButton } from "./components/quote-repost-button";
import { ExternalLinkIcon, ReplyIcon } from "../icons";
import NoteContentWithWarning from "./note-content-with-warning";
import { TrustProvider } from "../../providers/trust";
import { NoteLink } from "../note-link";
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
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 { getReferences, parseCoordinate } from "../../helpers/nostr/events";
import { getReferences } from "../../helpers/nostr/events";
import Timestamp from "../timestamp";
import OpenInDrawerButton from "../open-in-drawer-button";
import { getSharableEventAddress } from "../../helpers/nip19";
import { COMMUNITY_DEFINITION_KIND, getCommunityName } from "../../helpers/nostr/communities";
import { getCommunityName, getEventCommunityPointer } from "../../helpers/nostr/communities";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { useBreakpointValue } from "../../providers/breakpoint-provider";
import HoverLinkOverlay from "../hover-link-overlay";
@ -76,11 +75,8 @@ export const Note = React.memo(
// find mostr external link
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1];
const communityPointer = useMemo(() => {
const tag = event.tags.find((t) => isATag(t) && t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"));
return tag?.[1] ? parseCoordinate(tag[1], true) : undefined;
}, [event]);
const community = useReplaceableEvent(communityPointer);
const communityPointer = useMemo(() => getEventCommunityPointer(event), [event]);
const community = useReplaceableEvent(communityPointer ?? undefined);
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });

View File

@ -3,7 +3,7 @@ import { Flex, Heading, Link, SkeletonText, Text } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import { isATag, isETag, NostrEvent } from "../../../types/nostr-event";
import { isETag, NostrEvent } from "../../../types/nostr-event";
import { Note } from "../../note";
import NoteMenu from "../../note/note-menu";
import UserAvatar from "../../user-avatar";
@ -15,8 +15,8 @@ import { useRegisterIntersectionEntity } from "../../../providers/intersection-o
import useSingleEvent from "../../../hooks/use-single-event";
import { EmbedEvent } from "../../embed-event";
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
import { parseCoordinate, parseHardcodedNoteContent } from "../../../helpers/nostr/events";
import { COMMUNITY_DEFINITION_KIND } from "../../../helpers/nostr/communities";
import { parseHardcodedNoteContent } from "../../../helpers/nostr/events";
import { getEventCommunityPointer } from "../../../helpers/nostr/communities";
export default function RepostNote({ event }: { event: NostrEvent }) {
const ref = useRef<HTMLDivElement | null>(null);
@ -31,8 +31,7 @@ export default function RepostNote({ event }: { event: NostrEvent }) {
const loadedNote = useSingleEvent(eventId, readRelays);
const note = hardCodedNote || loadedNote;
const communityTag = event.tags.filter(isATag).find((t) => t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"));
const communityCoordinate = communityTag && parseCoordinate(communityTag[1]);
const communityCoordinate = getEventCommunityPointer(event);
if (note && muteFilter(note)) return;

View File

@ -1,7 +1,8 @@
import { validateEvent } from "nostr-tools";
import { NostrEvent, isDTag, isETag, isPTag } from "../../types/nostr-event";
import { NostrEvent, isATag, isDTag, isETag, isPTag } from "../../types/nostr-event";
import { getMatchLink, getMatchNostrLink } from "../regexp";
import { ReactionGroup } from "./reactions";
import { parseCoordinate } from "./events";
export const SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER = "communities";
export const COMMUNITY_DEFINITION_KIND = 34550;
@ -89,3 +90,8 @@ export function getCommunityPostVote(grouped: ReactionGroup[]) {
const vote = (up?.pubkeys.length ?? 0) - (down?.pubkeys.length ?? 0);
return { up, down, vote };
}
export function getEventCommunityPointer(event: NostrEvent){
const communityTag = event.tags.filter(isATag).find((t) => t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"));
return communityTag ? parseCoordinate(communityTag[1], true) : null;
}

View File

@ -131,8 +131,7 @@ function CommunitiesHomePage() {
const approvalMap = buildApprovalMap(events, mods);
const approved = events
.filter((event) => event.kind !== COMMUNITY_APPROVAL_KIND)
.filter((e) => (showUnapproved.isOpen ? true : approvalMap.has(e.id)))
.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && (showUnapproved.isOpen ? true : approvalMap.has(e.id)))
.map((event) => ({ event, approvals: approvalMap.get(event.id) }))
.filter((e) => !muteFilter(e.event));
@ -166,7 +165,7 @@ function CommunitiesHomePage() {
</Flex>
<IntersectionObserverProvider callback={callback}>
{approved.map(({ event, approvals }) => (
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} />
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} showCommunity />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
@ -202,10 +201,10 @@ function CommunitiesHomePage() {
<DrawerCloseButton />
<DrawerHeader p="4">Joined Communities</DrawerHeader>
<DrawerBody display="flex" flexDirection="column" gap="2" px="4" py="0">
<DrawerBody display="flex" flexDirection="column" gap="2" px="4" pt="0" pb="8" overflowY="auto">
{communities.map((community) => (
<ErrorBoundary key={getEventCoordinate(community)}>
<CommunityCard community={community} />
<CommunityCard community={community} flexShrink={0} />
</ErrorBoundary>
))}
</DrawerBody>

View File

@ -1,3 +1,4 @@
import { useContext } from "react";
import { Button, ButtonGroup, Divider, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
import { Kind, nip19 } from "nostr-tools";
@ -24,14 +25,16 @@ import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events";
import { WritingIcon } from "../../components/icons";
import { useContext } from "react";
import { PostModalContext } from "../../providers/post-modal-provider";
import CommunityEditModal from "./components/community-edit-modal";
import TimelineLoader from "../../classes/timeline-loader";
function getCommunityPath(community: NostrEvent) {
return `/c/${encodeURIComponent(getCommunityName(community))}/${nip19.npubEncode(community.pubkey)}`;
}
export type RouterContext = { community: NostrEvent; timeline: TimelineLoader };
export default function CommunityHomePage({ community }: { community: NostrEvent }) {
const image = getCommunityImage(community);
const location = useLocation();
@ -128,7 +131,7 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
</Button>
</ButtonGroup>
<Outlet context={{ community, timeline }} />
<Outlet context={{ community, timeline } satisfies RouterContext} />
</Flex>
{!verticalLayout && (

View File

@ -5,13 +5,15 @@ import PostVoteButtons from "./post-vote-buttions";
import CommunityPost from "./community-post";
import { NostrEvent } from "../../../types/nostr-event";
const ApprovedEvent = memo(({ event, approvals }: { event: NostrEvent; approvals: NostrEvent[] }) => {
return (
<Flex gap="2" alignItems="flex-start">
<PostVoteButtons event={event} flexShrink={0} />
<CommunityPost event={event} approvals={approvals} flex={1} />
</Flex>
);
});
const ApprovedEvent = memo(
({ event, approvals, showCommunity }: { event: NostrEvent; approvals: NostrEvent[]; showCommunity?: boolean }) => {
return (
<Flex gap="2" alignItems="flex-start">
<PostVoteButtons event={event} flexShrink={0} />
<CommunityPost event={event} approvals={approvals} flex={1} showCommunity={showCommunity} />
</Flex>
);
},
);
export default ApprovedEvent;

View File

@ -8,6 +8,7 @@ import {
CardProps,
Flex,
Heading,
Link,
LinkBox,
Text,
} from "@chakra-ui/react";
@ -16,7 +17,7 @@ import dayjs from "dayjs";
import { Kind } from "nostr-tools";
import { NostrEvent, isETag } from "../../../types/nostr-event";
import { getPostSubject } from "../../../helpers/nostr/communities";
import { getEventCommunityPointer, getPostSubject } from "../../../helpers/nostr/communities";
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
import { getSharableEventAddress } from "../../../helpers/nip19";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
@ -40,6 +41,7 @@ export function ApprovalIcon({ approval }: { approval: NostrEvent }) {
export type CommunityPostPropTypes = {
event: NostrEvent;
approvals: NostrEvent[];
showCommunity?: boolean;
};
function PostSubject({ event }: { event: NostrEvent }) {
@ -83,11 +85,14 @@ function Approvals({ approvals }: { approvals: NostrEvent[] }) {
export function CommunityTextPost({
event,
approvals,
showCommunity,
...props
}: Omit<CardProps, "children"> & CommunityPostPropTypes) {
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, getEventUID(event));
const communityPointer = getEventCommunityPointer(event);
return (
<Card as={LinkBox} ref={ref} {...props}>
<PostSubject event={event} />
@ -98,6 +103,14 @@ export function CommunityTextPost({
<Text>
Posted {dayjs.unix(event.created_at).fromNow()} by <UserLink pubkey={event.pubkey} fontWeight="bold" />
</Text>
{showCommunity && communityPointer && (
<Text>
to{" "}
<Link as={RouterLink} to={`/c/${communityPointer.identifier}/${communityPointer.pubkey}`} fontWeight="bold">
{communityPointer.identifier}
</Link>
</Text>
)}
<Flex gap="2" alignItems="center" ml="auto">
{approvals.length > 0 && <Approvals approvals={approvals} />}
<CommunityPostMenu event={event} approvals={approvals} aria-label="More Options" size="xs" variant="ghost" />
@ -110,6 +123,7 @@ export function CommunityTextPost({
export function CommunityRepostPost({
event,
approvals,
showCommunity,
...props
}: Omit<CardProps, "children"> & CommunityPostPropTypes) {
const encodedRepost = parseHardcodedNoteContent(event);
@ -126,6 +140,8 @@ export function CommunityRepostPost({
const muteFilter = useUserMuteFilter();
if (repost && muteFilter(repost)) return;
const communityPointer = getEventCommunityPointer(event);
return (
<Card as={LinkBox} ref={ref} {...props}>
{repost && (
@ -140,6 +156,14 @@ export function CommunityRepostPost({
<Text>
Shared {dayjs.unix(event.created_at).fromNow()} by <UserLink pubkey={event.pubkey} fontWeight="bold" />
</Text>
{showCommunity && communityPointer && (
<Text>
to{" "}
<Link as={RouterLink} to={`/c/${communityPointer.identifier}/${communityPointer.pubkey}`} fontWeight="bold">
{communityPointer.identifier}
</Link>
</Text>
)}
<Flex gap="2" alignItems="center" ml="auto">
{approvals.length > 0 && <Approvals approvals={approvals} />}
<CommunityPostMenu event={event} approvals={approvals} aria-label="More Options" size="xs" variant="ghost" />
@ -149,16 +173,12 @@ export function CommunityRepostPost({
);
}
export default function CommunityPost({
event,
approvals,
...props
}: Omit<CardProps, "children"> & CommunityPostPropTypes) {
export default function CommunityPost({ event, ...props }: Omit<CardProps, "children"> & CommunityPostPropTypes) {
switch (event.kind) {
case Kind.Text:
return <CommunityTextPost event={event} approvals={approvals} {...props} />;
return <CommunityTextPost event={event} {...props} />;
case Kind.Repost:
return <CommunityRepostPost event={event} approvals={approvals} {...props} />;
return <CommunityRepostPost event={event} {...props} />;
}
return null;
}

View File

@ -1,21 +1,16 @@
import { memo } from "react";
import { Flex } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { buildApprovalMap, getCommunityMods } from "../../../helpers/nostr/communities";
import { COMMUNITY_APPROVAL_KIND, buildApprovalMap, getCommunityMods } from "../../../helpers/nostr/communities";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import { NostrEvent } from "../../../types/nostr-event";
import IntersectionObserverProvider from "../../../providers/intersection-observer";
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
import PostVoteButtons from "../components/post-vote-buttions";
import TimelineLoader from "../../../classes/timeline-loader";
import CommunityPost from "../components/community-post";
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
import ApprovedEvent from "../components/community-approved-post";
import { RouterContext } from "../community-home";
export default function CommunityNewestView() {
const { community, timeline } = useOutletContext() as { community: NostrEvent; timeline: TimelineLoader };
const { community, timeline } = useOutletContext<RouterContext>();
const muteFilter = useUserMuteFilter();
const mods = getCommunityMods(community);
@ -23,7 +18,7 @@ export default function CommunityNewestView() {
const approvalMap = buildApprovalMap(events, mods);
const approved = events
.filter((e) => approvalMap.has(e.id))
.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && approvalMap.has(e.id))
.map((event) => ({ event, approvals: approvalMap.get(event.id) }))
.filter((e) => !muteFilter(e.event));

View File

@ -15,13 +15,13 @@ import useSubject from "../../../hooks/use-subject";
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
import TimelineLoader from "../../../classes/timeline-loader";
import { CheckIcon } from "../../../components/icons";
import { useSigningContext } from "../../../providers/signing-provider";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import { useWriteRelayUrls } from "../../../hooks/use-client-relays";
import CommunityPost from "../components/community-post";
import { RouterContext } from "../community-home";
type PendingProps = {
event: NostrEvent;
@ -84,7 +84,7 @@ function ModPendingPost({ event, community, approvals }: PendingProps) {
export default function CommunityPendingView() {
const account = useCurrentAccount();
const { community, timeline } = useOutletContext() as { community: NostrEvent; timeline: TimelineLoader };
const { community, timeline } = useOutletContext<RouterContext>();
const events = useSubject(timeline.timeline);

View File

@ -1,8 +1,8 @@
import { memo, useMemo } from "react";
import { Flex } from "@chakra-ui/react";
import { useMemo } from "react";
import { useOutletContext } from "react-router-dom";
import {
COMMUNITY_APPROVAL_KIND,
buildApprovalMap,
getCommunityMods,
getCommunityPostVote,
@ -10,19 +10,16 @@ import {
} from "../../../helpers/nostr/communities";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import { NostrEvent } from "../../../types/nostr-event";
import IntersectionObserverProvider from "../../../providers/intersection-observer";
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
import PostVoteButtons from "../components/post-vote-buttions";
import TimelineLoader from "../../../classes/timeline-loader";
import CommunityPost from "../components/community-post";
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
import useEventsReactions from "../../../hooks/use-events-reactions";
import { groupReactions } from "../../../helpers/nostr/reactions";
import ApprovedEvent from "../components/community-approved-post";
import { RouterContext } from "../community-home";
export default function CommunityTrendingView() {
const { community, timeline } = useOutletContext() as { community: NostrEvent; timeline: TimelineLoader };
const { community, timeline } = useOutletContext<RouterContext>();
const muteFilter = useUserMuteFilter();
const mods = getCommunityMods(community);
@ -30,7 +27,7 @@ export default function CommunityTrendingView() {
const approvalMap = buildApprovalMap(events, mods);
const approved = events
.filter((e) => approvalMap.has(e.id))
.filter((e) => e.kind !== COMMUNITY_APPROVAL_KIND && approvalMap.has(e.id))
.map((event) => ({ event, approvals: approvalMap.get(event.id) }))
.filter((e) => !muteFilter(e.event));