feat: hashtags header
This commit is contained in:
parent
e9b7c7c6e3
commit
540f29dd69
@ -47,7 +47,7 @@ export class FollowsFeedCache extends RefreshFeedCache<TaggedNostrEvent> {
|
||||
const keys = (await this.table?.toCollection().primaryKeys()) ?? [];
|
||||
this.onTable = new Set<string>(keys.map(a => a as string));
|
||||
|
||||
// load only latest 10 posts, rest can be loaded on-demand
|
||||
// load only latest 50 posts, rest can be loaded on-demand
|
||||
const latest = await this.table?.orderBy("created_at").reverse().limit(50).toArray();
|
||||
latest?.forEach(v => this.cache.set(this.key(v), v));
|
||||
|
||||
|
@ -12,6 +12,7 @@ import useLogin from "Hooks/useLogin";
|
||||
import { TimelineFragment, TimelineRenderer } from "./TimelineFragment";
|
||||
import useHashtagsFeed from "Feed/HashtagsFeed";
|
||||
import { ShowMoreInView } from "Element/Event/ShowMore";
|
||||
import { HashTagHeader } from "Pages/HashTagsPage";
|
||||
|
||||
export interface TimelineFollowsProps {
|
||||
postsOnly: boolean;
|
||||
@ -67,6 +68,7 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
const included = new Set<string>();
|
||||
return (mixin.data.data ?? [])
|
||||
.filter(a => !mainFeedIds.has(a.id) && postsOnly(a))
|
||||
.filter(a => a.tags.filter(a => a[0] === "t").length < 5)
|
||||
.reduce(
|
||||
(acc, v) => {
|
||||
if (included.has(v.id)) return acc;
|
||||
@ -116,9 +118,9 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
noteOnClick={props.noteOnClick}
|
||||
noteRenderer={props.noteRenderer}
|
||||
/>
|
||||
<ShowMoreInView
|
||||
{sortedFeed.length > 0 && <ShowMoreInView
|
||||
onClick={async () => await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at)}
|
||||
/>
|
||||
/>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -129,28 +131,32 @@ function weaveTimeline(
|
||||
hashtags: Record<string, Array<TaggedNostrEvent>>,
|
||||
): Array<TimelineFragment> {
|
||||
// always skip 5 posts from start to avoid heavy handed weaving
|
||||
const skip = 5;
|
||||
let skip = 5;
|
||||
|
||||
if (main.length < skip) {
|
||||
return [{ events: main, refTime: unixNow() }];
|
||||
skip = Math.min(skip, main.length - 1);
|
||||
}
|
||||
|
||||
const frags = Object.entries(hashtags).map(([k, v]) => {
|
||||
const take = v.slice(0, 5);
|
||||
return {
|
||||
title: (
|
||||
<div className="flex bb p">
|
||||
<h2>#{k}</h2>
|
||||
<div className="bb p">
|
||||
<HashTagHeader tag={k} />
|
||||
</div>
|
||||
),
|
||||
events: take,
|
||||
refTime: Math.min(
|
||||
main[skip].created_at,
|
||||
main.at(skip)?.created_at ?? unixNow(),
|
||||
take.reduce((acc, v) => (acc > v.created_at ? acc : v.created_at), 0),
|
||||
),
|
||||
} as TimelineFragment;
|
||||
});
|
||||
|
||||
if (main.length === 0) {
|
||||
return frags;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
events: main.slice(0, skip),
|
||||
@ -162,4 +168,4 @@ function weaveTimeline(
|
||||
refTime: main[skip].created_at,
|
||||
},
|
||||
].sort((a, b) => (a.refTime > b.refTime ? -1 : 1));
|
||||
}
|
||||
}
|
@ -39,6 +39,10 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.text iframe[src*="youtube.com"] {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2px;
|
||||
|
@ -1,51 +1,25 @@
|
||||
import { useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { NostrHashtagLink } from "@snort/system";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { EventKind, NostrHashtagLink, NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { dedupe } from "@snort/shared";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
import Timeline from "Element/Feed/Timeline";
|
||||
import useEventPublisher from "Hooks/useEventPublisher";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setTags } from "Login";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import ProfileImage from "Element/User/ProfileImage";
|
||||
|
||||
const HashTagsPage = () => {
|
||||
const params = useParams();
|
||||
const tag = (params.tag ?? "").toLowerCase();
|
||||
const login = useLogin();
|
||||
const isFollowing = useMemo(() => {
|
||||
return login.tags.item.includes(tag);
|
||||
}, [login, tag]);
|
||||
const { publisher, system } = useEventPublisher();
|
||||
|
||||
async function followTags(ts: string[]) {
|
||||
if (publisher) {
|
||||
const ev = await publisher.bookmarks(
|
||||
ts.map(a => new NostrHashtagLink(a)),
|
||||
"follow",
|
||||
);
|
||||
system.BroadcastEvent(ev);
|
||||
setTags(login, ts, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="main-content p">
|
||||
<div className="action-heading">
|
||||
<h2>#{tag}</h2>
|
||||
{isFollowing ? (
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
|
||||
<FormattedMessage defaultMessage="Unfollow" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={() => followTags(login.tags.item.concat([tag]))}>
|
||||
<FormattedMessage defaultMessage="Follow" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="bb p">
|
||||
<HashTagHeader tag={tag} />
|
||||
</div>
|
||||
<Timeline
|
||||
key={tag}
|
||||
@ -58,3 +32,58 @@ const HashTagsPage = () => {
|
||||
};
|
||||
|
||||
export default HashTagsPage;
|
||||
|
||||
|
||||
|
||||
export function HashTagHeader({ tag }: { tag: string }) {
|
||||
const login = useLogin();
|
||||
const isFollowing = useMemo(() => {
|
||||
return login.tags.item.includes(tag);
|
||||
}, [login, tag]);
|
||||
const { publisher, system } = useEventPublisher();
|
||||
|
||||
async function followTags(ts: string[]) {
|
||||
if (publisher) {
|
||||
const ev = await publisher.bookmarks(
|
||||
ts.map(a => new NostrHashtagLink(a)),
|
||||
"follow",
|
||||
);
|
||||
setTags(login, ts, ev.created_at * 1000);
|
||||
await system.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
const sub = useMemo(() => {
|
||||
const rb = new RequestBuilder(`hashtag-counts:${tag}`);
|
||||
rb.withFilter()
|
||||
.kinds([EventKind.CategorizedBookmarks])
|
||||
.tag("d", ["follow"])
|
||||
.tag("t", [tag.toLowerCase()]);
|
||||
return rb;
|
||||
}, [tag]);
|
||||
const followsTag = useRequestBuilder(NoteCollection, sub);
|
||||
const pubkeys = dedupe((followsTag.data ?? []).map(a => a.pubkey));
|
||||
|
||||
return <div className="flex items-center justify-between">
|
||||
<div className="flex flex-col g8">
|
||||
<h2>#{tag}</h2>
|
||||
<div className="flex">
|
||||
{pubkeys.slice(0, 5).map(a => <ProfileImage pubkey={a} showUsername={false} link={""} showFollowingMark={false} size={40} />)}
|
||||
{pubkeys.length > 5 && <span>
|
||||
+<FormattedNumber value={pubkeys.length - 5} />
|
||||
</span>}
|
||||
</div>
|
||||
</div>
|
||||
{isFollowing ? (
|
||||
<AsyncButton
|
||||
className="secondary"
|
||||
onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
|
||||
<FormattedMessage defaultMessage="Unfollow" />
|
||||
</AsyncButton>
|
||||
) : (
|
||||
<AsyncButton onClick={() => followTags(login.tags.item.concat([tag]))}>
|
||||
<FormattedMessage defaultMessage="Follow" />
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
}
|
@ -31,7 +31,6 @@ enum EventKind {
|
||||
CategorizedPeople = 30000, // NIP-51a
|
||||
CategorizedBookmarks = 30001, // NIP-51b
|
||||
|
||||
TagLists = 30002, // NIP-51c
|
||||
Badge = 30009, // NIP-58
|
||||
ProfileBadges = 30008, // NIP-58
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user