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