add feed / grid selector to feeds
This commit is contained in:
parent
c4273b9bdf
commit
ef4667c879
@ -1,6 +1,6 @@
|
||||
import "./Timeline.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { TaggedNostrEvent, EventKind } from "@snort/system";
|
||||
|
||||
import { dedupeByPubkey, findTag } from "@/SnortUtils";
|
||||
@ -8,7 +8,7 @@ import useTimelineFeed, { TimelineFeed, TimelineSubject } from "@/Feed/TimelineF
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
import { LiveStreams } from "@/Element/LiveStreams";
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { TimelineRenderer } from "@/Element/Feed/TimelineRenderer";
|
||||
import { DisplayAs, DisplayAsSelector, TimelineRenderer } from "@/Element/Feed/TimelineRenderer";
|
||||
|
||||
export interface TimelineProps {
|
||||
postsOnly: boolean;
|
||||
@ -19,6 +19,7 @@ export interface TimelineProps {
|
||||
now?: number;
|
||||
loadMore?: boolean;
|
||||
noSort?: boolean;
|
||||
displayAs?: DisplayAs;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,6 +34,7 @@ const Timeline = (props: TimelineProps) => {
|
||||
};
|
||||
}, [props]);
|
||||
const feed: TimelineFeed = useTimelineFeed(props.subject, feedOptions);
|
||||
const [displayAs, setDisplayAs] = useState<DisplayAs>("feed");
|
||||
|
||||
const { muted, isEventMuted } = useModeration();
|
||||
const filterPosts = useCallback(
|
||||
@ -70,6 +72,7 @@ const Timeline = (props: TimelineProps) => {
|
||||
return (
|
||||
<>
|
||||
<LiveStreams evs={liveStreams} />
|
||||
<DisplayAsSelector activeSelection={displayAs} onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)} />
|
||||
<TimelineRenderer
|
||||
frags={[
|
||||
{
|
||||
@ -80,6 +83,7 @@ const Timeline = (props: TimelineProps) => {
|
||||
related={feed.related ?? []}
|
||||
latest={latestAuthors}
|
||||
showLatest={t => onShowLatest(t)}
|
||||
displayAs={displayAs}
|
||||
/>
|
||||
{(props.loadMore === undefined || props.loadMore === true) && (
|
||||
<div className="flex items-center">
|
||||
|
@ -12,7 +12,7 @@ import { LiveStreams } from "@/Element/LiveStreams";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import useHashtagsFeed from "@/Feed/HashtagsFeed";
|
||||
import { ShowMoreInView } from "@/Element/Event/ShowMore";
|
||||
import { TimelineRenderer } from "@/Element/Feed/TimelineRenderer";
|
||||
import { DisplayAs, DisplayAsSelector, TimelineRenderer } from "@/Element/Feed/TimelineRenderer";
|
||||
|
||||
export interface TimelineFollowsProps {
|
||||
postsOnly: boolean;
|
||||
@ -20,12 +20,14 @@ export interface TimelineFollowsProps {
|
||||
noteFilter?: (ev: NostrEvent) => boolean;
|
||||
noteRenderer?: (ev: NostrEvent) => ReactNode;
|
||||
noteOnClick?: (ev: NostrEvent) => void;
|
||||
displayAs?: DisplayAs;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of notes by "subject"
|
||||
*/
|
||||
const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
const [displayAs, setDisplayAs] = useState<"feed" | "grid">("feed");
|
||||
const [latest, setLatest] = useState(unixNow());
|
||||
const feed = useSyncExternalStore(
|
||||
cb => FollowsFeed.hook(cb, "*"),
|
||||
@ -105,6 +107,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
return (
|
||||
<>
|
||||
{(props.liveStreams ?? true) && <LiveStreams evs={liveStreams} />}
|
||||
<DisplayAsSelector activeSelection={displayAs} onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)} />
|
||||
<TimelineRenderer
|
||||
frags={[{ events: orderDescending(mainFeed.concat(mixinFiltered)), refTime: latest }]}
|
||||
related={reactions.data ?? []}
|
||||
@ -117,6 +120,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
return <Link to={`/t/${e.context}`}>{`#${e.context}`}</Link>;
|
||||
}
|
||||
}}
|
||||
displayAs={displayAs}
|
||||
/>
|
||||
{sortedFeed.length > 0 && (
|
||||
<ShowMoreInView onClick={async () => await FollowsFeed.loadMore(system, login, oldest ?? unixNow())} />
|
||||
@ -124,4 +128,5 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineFollows;
|
||||
|
@ -2,12 +2,14 @@ import { useInView } from "react-intersection-observer";
|
||||
import ProfileImage from "@/Element/User/ProfileImage";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import {TaggedNostrEvent} from "@snort/system";
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import { ReactNode } from "react";
|
||||
import { TimelineFragment } from "@/Element/Feed/TimelineFragment";
|
||||
import {transformTextCached} from "@/Hooks/useTextTransformCache";
|
||||
import { transformTextCached } from "@/Hooks/useTextTransformCache";
|
||||
import useImgProxy from "@/Hooks/useImgProxy";
|
||||
|
||||
export type DisplayAs = "grid" | "feed";
|
||||
|
||||
export interface TimelineRendererProps {
|
||||
frags: Array<TimelineFragment>;
|
||||
related: Array<TaggedNostrEvent>;
|
||||
@ -19,7 +21,7 @@ export interface TimelineRendererProps {
|
||||
noteRenderer?: (ev: TaggedNostrEvent) => ReactNode;
|
||||
noteOnClick?: (ev: TaggedNostrEvent) => void;
|
||||
noteContext?: (ev: TaggedNostrEvent) => ReactNode;
|
||||
displayAs?: "grid" | "feed";
|
||||
displayAs?: DisplayAs;
|
||||
}
|
||||
|
||||
export function TimelineRenderer(props: TimelineRendererProps) {
|
||||
@ -49,17 +51,14 @@ export function TimelineRenderer(props: TimelineRendererProps) {
|
||||
className="aspect-square bg-center bg-cover cursor-pointer"
|
||||
key={e.id}
|
||||
style={{ backgroundImage: `url(${proxy(images[0].content)})` }}
|
||||
onClick={() => props.noteOnClick?.(e)}
|
||||
></div>
|
||||
onClick={() => props.noteOnClick?.(e)}></div>
|
||||
);
|
||||
};
|
||||
|
||||
const noteRenderer = props.noteRenderer || noteImageRenderer;
|
||||
|
||||
return props.frags.map(frag => (
|
||||
<div className="grid grid-cols-3 gap-1 p-1">
|
||||
{frag.events.map(event => noteRenderer(event))}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1 p-1">{frag.events.map(event => noteRenderer(event))}</div>
|
||||
));
|
||||
};
|
||||
|
||||
@ -99,3 +98,29 @@ export function TimelineRenderer(props: TimelineRendererProps) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type DisplaySelectorProps = {
|
||||
activeSelection: DisplayAs;
|
||||
onSelect: (display: DisplayAs) => void;
|
||||
};
|
||||
|
||||
export const DisplayAsSelector = ({ activeSelection, onSelect }: DisplaySelectorProps) => {
|
||||
return (
|
||||
<div className="flex mb-4">
|
||||
<div
|
||||
className={`border-highlight cursor-pointer flex justify-center flex-1 p-3 ${
|
||||
activeSelection === "feed" ? "border-b border-1" : "hover:bg-nearly-bg-color text-secondary"
|
||||
}`}
|
||||
onClick={() => onSelect("feed")}>
|
||||
<FormattedMessage defaultMessage="Feed" id="eW/Bj9" />
|
||||
</div>
|
||||
<div
|
||||
className={`border-highlight cursor-pointer flex justify-center flex-1 p-3 ${
|
||||
activeSelection === "grid" ? "border-b border-1" : "hover:bg-nearly-bg-color text-secondary"
|
||||
}`}
|
||||
onClick={() => onSelect("grid")}>
|
||||
<FormattedMessage defaultMessage="Grid" id="HzfrYu" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -12,7 +12,6 @@ import TimelineFollows from "@/Element/Feed/TimelineFollows";
|
||||
import { transformTextCached } from "@/Hooks/useTextTransformCache";
|
||||
import Icon from "@/Icons/Icon";
|
||||
import NotificationsPage from "./Notifications/Notifications";
|
||||
import useImgProxy from "@/Hooks/useImgProxy";
|
||||
import Modal from "@/Element/Modal";
|
||||
import { Thread } from "@/Element/Event/Thread";
|
||||
import { RootTabs } from "@/Element/Feed/RootTabs";
|
||||
@ -159,36 +158,23 @@ function ArticlesCol() {
|
||||
}
|
||||
|
||||
function MediaCol({ setThread }: { setThread: (e: NostrLink) => void }) {
|
||||
const { proxy } = useImgProxy();
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 p-2 border-b border-border-color">
|
||||
<Icon name="camera-lens" size={24} />
|
||||
<FormattedMessage defaultMessage="Media" id="hmZ3Bz" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1 p-1">
|
||||
<TimelineFollows
|
||||
postsOnly={true}
|
||||
liveStreams={false}
|
||||
noteFilter={e => {
|
||||
const parsed = transformTextCached(e.id, e.content, e.tags);
|
||||
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
|
||||
return images.length > 0;
|
||||
}}
|
||||
noteRenderer={e => {
|
||||
const parsed = transformTextCached(e.id, e.content, e.tags);
|
||||
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="aspect-square bg-center bg-cover cursor-pointer"
|
||||
key={e.id}
|
||||
style={{ backgroundImage: `url(${proxy(images[0].content)})` }}
|
||||
onClick={() => setThread(NostrLink.fromEvent(e))}></div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TimelineFollows
|
||||
postsOnly={true}
|
||||
liveStreams={false}
|
||||
noteFilter={e => {
|
||||
const parsed = transformTextCached(e.id, e.content, e.tags);
|
||||
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));
|
||||
return images.length > 0;
|
||||
}}
|
||||
displayAs="grid"
|
||||
noteOnClick={e => setThread(NostrLink.fromEvent(e))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -534,6 +534,9 @@
|
||||
"HhcAVH": {
|
||||
"defaultMessage": "You don't follow this person, click here to load media from <i>{link}</i>, or update <a><i>your preferences</i></a> to always load media from everybody."
|
||||
},
|
||||
"HzfrYu": {
|
||||
"defaultMessage": "Grid"
|
||||
},
|
||||
"IEwZvs": {
|
||||
"defaultMessage": "Are you sure you want to unpin this note?"
|
||||
},
|
||||
@ -1059,6 +1062,9 @@
|
||||
"eSzf2G": {
|
||||
"defaultMessage": "A single zap of {nIn} sats will allocate {nOut} sats to the zap pool."
|
||||
},
|
||||
"eW/Bj9": {
|
||||
"defaultMessage": "Feed"
|
||||
},
|
||||
"eXT2QQ": {
|
||||
"defaultMessage": "Group Chat"
|
||||
},
|
||||
|
@ -176,6 +176,7 @@
|
||||
"HWbkEK": "Clear cache and reload",
|
||||
"HbefNb": "Open Wallet",
|
||||
"HhcAVH": "You don't follow this person, click here to load media from <i>{link}</i>, or update <a><i>your preferences</i></a> to always load media from everybody.",
|
||||
"HzfrYu": "Grid",
|
||||
"IEwZvs": "Are you sure you want to unpin this note?",
|
||||
"IKKHqV": "Follows",
|
||||
"IVbtTS": "Zap all {n} sats",
|
||||
@ -348,6 +349,7 @@
|
||||
"eHAneD": "Reaction emoji",
|
||||
"eJj8HD": "Get Verified",
|
||||
"eSzf2G": "A single zap of {nIn} sats will allocate {nOut} sats to the zap pool.",
|
||||
"eW/Bj9": "Feed",
|
||||
"eXT2QQ": "Group Chat",
|
||||
"egib+2": "{n,plural,=1{& {n} other} other{& {n} others}}",
|
||||
"fBI91o": "Zap",
|
||||
|
@ -7,6 +7,7 @@ module.exports = {
|
||||
colors: {
|
||||
"nearly-bg-color": "var(--nearly-bg-color)",
|
||||
"border-color": "var(--border-color)",
|
||||
highlight: "var(--highlight)",
|
||||
},
|
||||
textColor: {
|
||||
"nostr-blue": "var(--repost)",
|
||||
@ -14,6 +15,7 @@ module.exports = {
|
||||
"nostr-orange": "var(--zap)",
|
||||
"nostr-red": "var(--heart)",
|
||||
"nostr-purple": "var(--highlight)",
|
||||
secondary: "var(--font-secondary-color)",
|
||||
},
|
||||
spacing: {
|
||||
px: "1px",
|
||||
|
Loading…
Reference in New Issue
Block a user