trending notes showable as grid
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
6ec5911b7c
commit
a1b2966188
41
packages/app/src/Element/Feed/ImageGridItem.tsx
Normal file
41
packages/app/src/Element/Feed/ImageGridItem.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { MouseEvent } from "react";
|
||||||
|
import useImgProxy from "@/Hooks/useImgProxy";
|
||||||
|
import { transformTextCached } from "@/Hooks/useTextTransformCache";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Icon from "@/Icons/Icon";
|
||||||
|
|
||||||
|
const ImageGridItem = (props: { event: TaggedNostrEvent; onClick: (e: MouseEvent) => void }) => {
|
||||||
|
const { event, onClick } = props;
|
||||||
|
const { proxy } = useImgProxy();
|
||||||
|
|
||||||
|
const parsed = transformTextCached(event.id, event.content, event.tags);
|
||||||
|
const media = parsed.filter(
|
||||||
|
a => a.type === "media" && (a.mimeType?.startsWith("image/") || a.mimeType?.startsWith("video/")),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (media.length === 0) return null;
|
||||||
|
|
||||||
|
const multiple = media.length > 1;
|
||||||
|
const isVideo = media[0].mimeType?.startsWith("video/");
|
||||||
|
const noteId = NostrLink.fromEvent(event).encode(CONFIG.eventLinkPrefix);
|
||||||
|
|
||||||
|
const myOnClick = (clickEvent: MouseEvent) => {
|
||||||
|
if (onClick) {
|
||||||
|
onClick(clickEvent);
|
||||||
|
clickEvent.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={`/${noteId}`} className="aspect-square cursor-pointer hover:opacity-80 relative" onClick={myOnClick}>
|
||||||
|
<img src={proxy(media[0].content, 256)} alt="Note Media" className="w-full h-full object-cover" />
|
||||||
|
<div className="absolute right-2 top-2 flex flex-col gap-2">
|
||||||
|
{multiple && <Icon name="copy-solid" className="text-white opacity-80 drop-shadow-md" />}
|
||||||
|
{isVideo && <Icon name="play-square-outline" className="text-white opacity-80 drop-shadow-md" />}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageGridItem;
|
@ -142,7 +142,7 @@ export function RootTabs({ base = "/" }) {
|
|||||||
if (currentTab && currentTab !== rootType) {
|
if (currentTab && currentTab !== rootType) {
|
||||||
setRootType(currentTab);
|
setRootType(currentTab);
|
||||||
}
|
}
|
||||||
}, [location, menuItems, rootType]);
|
}, [location.pathname, menuItems, rootType]);
|
||||||
|
|
||||||
function currentMenuItem() {
|
function currentMenuItem() {
|
||||||
if (location.pathname.startsWith(`${base}/t/`)) {
|
if (location.pathname.startsWith(`${base}/t/`)) {
|
||||||
|
@ -5,11 +5,9 @@ import Icon from "@/Icons/Icon";
|
|||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { TimelineFragment } from "@/Element/Feed/TimelineFragment";
|
import { TimelineFragment } from "@/Element/Feed/TimelineFragment";
|
||||||
import { transformTextCached } from "@/Hooks/useTextTransformCache";
|
|
||||||
import useImgProxy from "@/Hooks/useImgProxy";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { DisplayAs } from "@/Element/Feed/DisplayAsSelector";
|
import { DisplayAs } from "@/Element/Feed/DisplayAsSelector";
|
||||||
import { SpotlightThreadModal } from "@/Element/Spotlight/SpotlightThreadModal";
|
import { SpotlightThreadModal } from "@/Element/Spotlight/SpotlightThreadModal";
|
||||||
|
import ImageGridItem from "@/Element/Feed/ImageGridItem";
|
||||||
|
|
||||||
export interface TimelineRendererProps {
|
export interface TimelineRendererProps {
|
||||||
frags: Array<TimelineFragment>;
|
frags: Array<TimelineFragment>;
|
||||||
@ -27,7 +25,6 @@ export interface TimelineRendererProps {
|
|||||||
|
|
||||||
export function TimelineRenderer(props: TimelineRendererProps) {
|
export function TimelineRenderer(props: TimelineRendererProps) {
|
||||||
const { ref, inView } = useInView();
|
const { ref, inView } = useInView();
|
||||||
const { proxy } = useImgProxy();
|
|
||||||
const [modalThread, setModalThread] = useState<NostrLink | undefined>(undefined);
|
const [modalThread, setModalThread] = useState<NostrLink | undefined>(undefined);
|
||||||
|
|
||||||
const renderNotes = () => {
|
const renderNotes = () => {
|
||||||
@ -44,43 +41,13 @@ export function TimelineRenderer(props: TimelineRendererProps) {
|
|||||||
|
|
||||||
const renderGrid = () => {
|
const renderGrid = () => {
|
||||||
// TODO Hide images from notes with a content warning, unless otherwise configured
|
// TODO Hide images from notes with a content warning, unless otherwise configured
|
||||||
const noteImageRenderer = (e: TaggedNostrEvent) => {
|
|
||||||
const parsed = transformTextCached(e.id, e.content, e.tags);
|
|
||||||
const media = parsed.filter(
|
|
||||||
a => a.type === "media" && (a.mimeType?.startsWith("image/") || a.mimeType?.startsWith("video/")),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (media.length === 0) return null;
|
|
||||||
|
|
||||||
const multiple = media.length > 1;
|
|
||||||
const isVideo = media[0].mimeType?.startsWith("video/");
|
|
||||||
const noteId = NostrLink.fromEvent(e).encode(CONFIG.eventLinkPrefix);
|
|
||||||
|
|
||||||
const onClick = (clickEvent: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
|
||||||
if (props.noteOnClick) {
|
|
||||||
props.noteOnClick(e);
|
|
||||||
clickEvent.preventDefault();
|
|
||||||
} else if (window.innerWidth >= 768) {
|
|
||||||
setModalThread(NostrLink.fromEvent(e));
|
|
||||||
clickEvent.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={`/${noteId}`} className="aspect-square cursor-pointer hover:opacity-80 relative" onClick={onClick}>
|
|
||||||
<img src={proxy(media[0].content, 256)} alt="Note Media" className="w-full h-full object-cover" />
|
|
||||||
<div className="absolute right-2 top-2 flex flex-col gap-2">
|
|
||||||
{multiple && <Icon name="copy-solid" className="text-white opacity-80 drop-shadow-md" />}
|
|
||||||
{isVideo && <Icon name="play-square-outline" className="text-white opacity-80 drop-shadow-md" />}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const noteRenderer = props.noteRenderer || noteImageRenderer;
|
|
||||||
|
|
||||||
return props.frags.map(frag => (
|
return props.frags.map(frag => (
|
||||||
<div className="grid grid-cols-3 gap-px md:gap-1">{frag.events.map(event => noteRenderer(event))}</div>
|
<div className="grid grid-cols-3 gap-px md:gap-1">
|
||||||
|
{frag.events.map(event => (
|
||||||
|
<ImageGridItem event={event} onClick={() => setModalThread(NostrLink.fromEvent(event))} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ import { useLocale } from "@/IntlProvider";
|
|||||||
import useModeration from "@/Hooks/useModeration";
|
import useModeration from "@/Hooks/useModeration";
|
||||||
import ShortNote from "@/Element/Trending/ShortNote";
|
import ShortNote from "@/Element/Trending/ShortNote";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { DisplayAs, DisplayAsSelector } from "@/Element/Feed/DisplayAsSelector";
|
||||||
|
import ImageGridItem from "@/Element/Feed/ImageGridItem";
|
||||||
|
|
||||||
export default function TrendingNotes({ count = Infinity, small = false }) {
|
export default function TrendingNotes({ count = Infinity, small = false }) {
|
||||||
// Added count prop with a default value
|
// Added count prop with a default value
|
||||||
@ -17,6 +19,7 @@ export default function TrendingNotes({ count = Infinity, small = false }) {
|
|||||||
const [error, setError] = useState<Error>();
|
const [error, setError] = useState<Error>();
|
||||||
const { lang } = useLocale();
|
const { lang } = useLocale();
|
||||||
const { isEventMuted } = useModeration();
|
const { isEventMuted } = useModeration();
|
||||||
|
const [displayAs, setDisplayAs] = useState<DisplayAs>("list");
|
||||||
const related = useReactions("trending", posts?.map(a => NostrLink.fromEvent(a)) ?? [], undefined, true);
|
const related = useReactions("trending", posts?.map(a => NostrLink.fromEvent(a)) ?? [], undefined, true);
|
||||||
|
|
||||||
async function loadTrendingNotes() {
|
async function loadTrendingNotes() {
|
||||||
@ -46,18 +49,34 @@ export default function TrendingNotes({ count = Infinity, small = false }) {
|
|||||||
showContextMenu: !small,
|
showContextMenu: !small,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredAndLimitedPosts = () => {
|
||||||
|
return posts.filter(a => !isEventMuted(a)).slice(0, count);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderGrid = () => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 gap-px md:gap-1">
|
||||||
|
{filteredAndLimitedPosts().map(e => (
|
||||||
|
<ImageGridItem event={e as TaggedNostrEvent} onClick={() => {}} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderList = () => {
|
||||||
|
return filteredAndLimitedPosts().map(e =>
|
||||||
|
small ? (
|
||||||
|
<ShortNote event={e as TaggedNostrEvent} />
|
||||||
|
) : (
|
||||||
|
<Note data={e as TaggedNostrEvent} related={related?.data ?? []} depth={0} options={options} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("flex flex-col", { "gap-6": small, "py-4": small })}>
|
<div className={classNames("flex flex-col", { "gap-6": small, "py-4": small })}>
|
||||||
{posts
|
{!small && <DisplayAsSelector activeSelection={displayAs} onSelect={a => setDisplayAs(a)} />}
|
||||||
.filter(a => !isEventMuted(a))
|
{displayAs === "grid" ? renderGrid() : renderList()}
|
||||||
.slice(0, count) // Limit the number of posts displayed
|
|
||||||
.map(e =>
|
|
||||||
small ? (
|
|
||||||
<ShortNote key={e.id} event={e as TaggedNostrEvent} />
|
|
||||||
) : (
|
|
||||||
<Note key={e.id} data={e as TaggedNostrEvent} related={related?.data ?? []} depth={0} options={options} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user