feat: hashtags header

This commit is contained in:
Kieran 2023-11-13 15:58:14 +00:00
parent e9b7c7c6e3
commit 540f29dd69
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
5 changed files with 82 additions and 44 deletions

View File

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

View File

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

View File

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

View File

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

View File

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