add feed / grid selector to feeds

This commit is contained in:
Martti Malmi 2023-11-28 21:41:53 +02:00
parent c4273b9bdf
commit ef4667c879
7 changed files with 66 additions and 36 deletions

View File

@ -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">

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -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"
},

View File

@ -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",

View File

@ -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",