mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Add reads sidebar
This commit is contained in:
parent
219f3ab084
commit
b1ad4299eb
@ -56,3 +56,23 @@
|
||||
margin-top: 34px;
|
||||
z-index: 10px;
|
||||
}
|
||||
|
||||
.headingPicks {
|
||||
@include heading();
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
.readsSidebar {
|
||||
.topic {
|
||||
display: inline-block;
|
||||
background-color: var(--background-input);
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
203
src/components/HomeSidebar/ReadsSidebar.tsx
Normal file
203
src/components/HomeSidebar/ReadsSidebar.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import { Component, createEffect, createSignal, For, onMount, Show } from 'solid-js';
|
||||
|
||||
import {
|
||||
EventCoordinate,
|
||||
PrimalArticle,
|
||||
SelectionOption
|
||||
} from '../../types/primal';
|
||||
|
||||
import styles from './HomeSidebar.module.scss';
|
||||
import SmallNote from '../SmallNote/SmallNote';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import SelectionBox from '../SelectionBox/SelectionBox';
|
||||
import Loader from '../Loader/Loader';
|
||||
import { readHomeSidebarSelection, saveHomeSidebarSelection } from '../../lib/localStore';
|
||||
import { useHomeContext } from '../../contexts/HomeContext';
|
||||
import { useReadsContext } from '../../contexts/ReadsContext';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { APP_ID } from '../../App';
|
||||
import { subsTo } from '../../sockets';
|
||||
import { getArticleThread, getReadsTopics } from '../../lib/feed';
|
||||
import { fetchArticles } from '../../handleNotes';
|
||||
import { getParametrizedEvent, getParametrizedEvents } from '../../lib/notes';
|
||||
import { decodeIdentifier } from '../../lib/keys';
|
||||
|
||||
const sidebarOptions = [
|
||||
{
|
||||
label: 'Trending 24h',
|
||||
value: 'trending_24h',
|
||||
},
|
||||
{
|
||||
label: 'Trending 12h',
|
||||
value: 'trending_12h',
|
||||
},
|
||||
{
|
||||
label: 'Trending 4h',
|
||||
value: 'trending_4h',
|
||||
},
|
||||
{
|
||||
label: 'Trending 1h',
|
||||
value: 'trending_1h',
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
value: '',
|
||||
disabled: true,
|
||||
separator: true,
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Most-zapped 24h',
|
||||
value: 'mostzapped_24h',
|
||||
},
|
||||
{
|
||||
label: 'Most-zapped 12h',
|
||||
value: 'mostzapped_12h',
|
||||
},
|
||||
{
|
||||
label: 'Most-zapped 4h',
|
||||
value: 'mostzapped_4h',
|
||||
},
|
||||
{
|
||||
label: 'Most-zapped 1h',
|
||||
value: 'mostzapped_1h',
|
||||
},
|
||||
];
|
||||
|
||||
const ReadsSidebar: Component< { id?: string } > = (props) => {
|
||||
|
||||
const account = useAccountContext();
|
||||
const reads= useReadsContext();
|
||||
|
||||
const [topPicks, setTopPicks] = createStore<PrimalArticle[]>([]);
|
||||
const [topics, setTopics] = createStore<string[]>([]);
|
||||
|
||||
const [isFetching, setIsFetching] = createSignal(false);
|
||||
const [isFetchingTopics, setIsFetchingTopics] = createSignal(false);
|
||||
|
||||
|
||||
|
||||
const [got, setGot] = createSignal(false);
|
||||
|
||||
const getTopics = () => {
|
||||
const subId = `reads_topics_${APP_ID}`;
|
||||
|
||||
const unsub = subsTo(subId, {
|
||||
onEvent: (_, content) => {
|
||||
const topics = JSON.parse(content.content || '[]') as string[];
|
||||
|
||||
setTopics(() => [...topics]);
|
||||
},
|
||||
onEose: () => {
|
||||
setIsFetchingTopics(() => false);
|
||||
unsub();
|
||||
}
|
||||
})
|
||||
setIsFetchingTopics(() => true);
|
||||
getReadsTopics(subId);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (account?.isKeyLookupDone && reads?.recomendedReads.length === 0) {
|
||||
reads.actions.doSidebarSearch('');
|
||||
}
|
||||
|
||||
if (account?.isKeyLookupDone) {
|
||||
getTopics()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
const rec = reads?.recomendedReads || [];
|
||||
|
||||
if (rec.length > 0 && !got()) {
|
||||
setGot(() => true);
|
||||
let randomIndices = new Set<number>();
|
||||
|
||||
while (randomIndices.size < 3) {
|
||||
const randomIndex = Math.floor(Math.random() * rec.length);
|
||||
randomIndices.add(randomIndex);
|
||||
}
|
||||
|
||||
const reads = [ ...randomIndices ].map(i => rec[i])
|
||||
getPEvents(reads)
|
||||
// getRecomendedArticles(reads)
|
||||
}
|
||||
})
|
||||
|
||||
const getPEvents = (ids: string[]) => {
|
||||
const events = ids.reduce<EventCoordinate[]>((acc, id) => {
|
||||
const d = decodeIdentifier(id);
|
||||
|
||||
if (!d.data || d.type !== 'naddr') return acc;
|
||||
|
||||
const { pubkey, identifier, kind } = d.data;
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{ identifier, pubkey, kind },
|
||||
]
|
||||
|
||||
}, []);
|
||||
|
||||
getParametrizedEvents(events, `reads_pe_${APP_ID}`);
|
||||
}
|
||||
|
||||
const getRecomendedArticles = async (ids: string[]) => {
|
||||
if (!account?.publicKey) return;
|
||||
|
||||
const subId = `reads_picks_${APP_ID}`;
|
||||
|
||||
setIsFetching(() => true);
|
||||
|
||||
const articles = await fetchArticles(account.publicKey, ids,subId);
|
||||
|
||||
setIsFetching(() => false);
|
||||
|
||||
console.log('ARTICLES: ', articles);
|
||||
|
||||
setTopPicks(() => [...articles]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={props.id} class={styles.readsSidebar}>
|
||||
<Show when={account?.isKeyLookupDone}>
|
||||
<div class={styles.headingPicks}>
|
||||
Top Picks
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={
|
||||
<Loader />
|
||||
}
|
||||
>
|
||||
<For each={topPicks}>
|
||||
{(note) => <div>{note.title}</div>}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
|
||||
<div class={styles.headingPicks}>
|
||||
Topics
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={!isFetchingTopics()}
|
||||
fallback={
|
||||
<Loader />
|
||||
}
|
||||
>
|
||||
<For each={topics}>
|
||||
{(topic) => <div class={styles.topic}>{topic}</div>}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ReadsSidebar);
|
@ -6,7 +6,7 @@ import { Kind, minKnownProfiles } from "../constants";
|
||||
import { getArticlesFeed, getEvents, getExploreFeed, getFeed, getFutureArticlesFeed, getFutureExploreFeed, getFutureFeed } from "../lib/feed";
|
||||
import { fetchStoredFeed, saveStoredFeed } from "../lib/localStore";
|
||||
import { setLinkPreviews } from "../lib/notes";
|
||||
import { getScoredUsers, searchContent } from "../lib/search";
|
||||
import { getRecomendedArticleIds, getScoredUsers, searchContent } from "../lib/search";
|
||||
import { isConnected, refreshSocketListeners, removeSocketListeners, socket } from "../sockets";
|
||||
import { sortingPlan, convertToNotes, parseEmptyReposts, paginationPlan, isInTags, isRepostInCollection, convertToArticles, isLFRepostInCollection } from "../stores/note";
|
||||
import {
|
||||
@ -41,6 +41,7 @@ type ReadsContextStore = {
|
||||
lastNote: PrimalArticle | undefined,
|
||||
reposts: Record<string, string> | undefined,
|
||||
mentionedNotes: Record<string, NostrNoteContent>,
|
||||
recomendedReads: string[],
|
||||
future: {
|
||||
notes: PrimalArticle[],
|
||||
page: FeedPage,
|
||||
@ -119,6 +120,7 @@ const initialHomeData = {
|
||||
isFetching: false,
|
||||
query: undefined,
|
||||
},
|
||||
recomendedReads: [],
|
||||
};
|
||||
|
||||
export const ReadsContext = createContext<ReadsContextStore>();
|
||||
@ -201,13 +203,13 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
};
|
||||
|
||||
const doSidebarSearch = (query: string) => {
|
||||
const subid = `reads_sidebar_${APP_ID}`;
|
||||
const subid = `reads_recomended_${APP_ID}`;
|
||||
|
||||
updateStore('sidebar', 'isFetching', () => true);
|
||||
updateStore('sidebar', 'notes', () => []);
|
||||
updateStore('sidebar', 'page', { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {} });
|
||||
updateStore('sidebar', 'page', { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {}, topZaps: {} });
|
||||
|
||||
getScoredUsers(account?.publicKey, query, 10, subid);
|
||||
getRecomendedArticleIds(subid);
|
||||
}
|
||||
|
||||
const clearFuture = () => {
|
||||
@ -647,6 +649,27 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
|
||||
const [type, subId, content] = message;
|
||||
|
||||
if (subId === `reads_recomended_${APP_ID}`) {
|
||||
if (type === 'EOSE') {
|
||||
// saveSidebarPage(store.sidebar.page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'EVENT') {
|
||||
const recomended = JSON.parse(content?.content || '{}');
|
||||
const ids = recomended.reads.reduce((acc: string[], r: string[]) => r[0] ? [ ...acc, r[0] ] : acc, []);
|
||||
|
||||
updateStore('recomendedReads', () => [ ...ids ])
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (subId === `reads_sidebar_${APP_ID}`) {
|
||||
if (type === 'EOSE') {
|
||||
saveSidebarPage(store.sidebar.page);
|
||||
|
@ -4,17 +4,15 @@ import { getEvents } from "./lib/feed";
|
||||
import { setLinkPreviews } from "./lib/notes";
|
||||
import { updateStore, store } from "./services/StoreService";
|
||||
import { subscribeTo } from "./sockets";
|
||||
import { convertToNotes } from "./stores/note";
|
||||
import { convertToArticles, convertToNotes } from "./stores/note";
|
||||
import { account } from "./translations";
|
||||
import { FeedPage, NostrEventContent, NostrEventType, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalNote, TopZap } from "./types/primal";
|
||||
import { FeedPage, NostrEventContent, NostrEventType, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalArticle, PrimalNote, TopZap } from "./types/primal";
|
||||
import { parseBolt11 } from "./utils";
|
||||
|
||||
export const fetchNotes = (pubkey: string | undefined, noteIds: string[], subId: string) => {
|
||||
return new Promise<PrimalNote[]>((resolve, reject) => {
|
||||
if (!pubkey) reject('Missing pubkey');
|
||||
|
||||
let note: PrimalNote;
|
||||
|
||||
let page: FeedPage = {
|
||||
users: {},
|
||||
messages: [],
|
||||
@ -176,6 +174,178 @@ export const fetchNotes = (pubkey: string | undefined, noteIds: string[], subId:
|
||||
const quoteStats = JSON.parse(content.content);
|
||||
|
||||
|
||||
// updateStore('quoteCount', () => quoteStats.count || 0);
|
||||
return;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchArticles = (pubkey: string | undefined, noteIds: string[], subId: string) => {
|
||||
return new Promise<PrimalArticle[]>((resolve, reject) => {
|
||||
if (!pubkey) reject('Missing pubkey');
|
||||
|
||||
let page: FeedPage = {
|
||||
users: {},
|
||||
messages: [],
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
relayHints: {},
|
||||
topZaps: {},
|
||||
since: 0,
|
||||
until: 0,
|
||||
}
|
||||
|
||||
let lastNote: PrimalArticle | undefined;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, _, content) => {
|
||||
|
||||
if (type === 'EOSE') {
|
||||
unsub();
|
||||
const notes = convertToArticles(page, page.topZaps);
|
||||
|
||||
resolve(notes);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (!content) return;
|
||||
updatePage(content);
|
||||
}
|
||||
});
|
||||
|
||||
getEvents(pubkey, [...noteIds], subId, true);
|
||||
|
||||
const updatePage = (content: NostrEventContent) => {
|
||||
if (content.kind === Kind.Metadata) {
|
||||
const user = content as NostrUserContent;
|
||||
|
||||
page.users[user.pubkey] = { ...user };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ([Kind.LongForm, Kind.Repost].includes(content.kind)) {
|
||||
const message = content as NostrNoteContent;
|
||||
|
||||
if (lastNote?.noteId !== nip19.noteEncode(message.id)) {
|
||||
page.messages.push({...message});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.NoteStats) {
|
||||
const statistic = content as NostrStatsContent;
|
||||
const stat = JSON.parse(statistic.content);
|
||||
page.postStats[stat.event_id] = { ...stat };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.Mentions) {
|
||||
const mentionContent = content as NostrMentionContent;
|
||||
const mention = JSON.parse(mentionContent.content);
|
||||
|
||||
if (!page.mentions) {
|
||||
page.mentions = {};
|
||||
}
|
||||
|
||||
page.mentions[mention.id] = { ...mention };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.NoteActions) {
|
||||
const noteActionContent = content as NostrNoteActionsContent;
|
||||
const noteActions = JSON.parse(noteActionContent.content) as NoteActions;
|
||||
|
||||
page.noteActions[noteActions.event_id] = { ...noteActions };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.LinkMetadata) {
|
||||
const metadata = JSON.parse(content.content);
|
||||
|
||||
const data = metadata.resources[0];
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = {
|
||||
url: data.url,
|
||||
title: data.md_title,
|
||||
description: data.md_description,
|
||||
mediaType: data.mimetype,
|
||||
contentType: data.mimetype,
|
||||
images: [data.md_image],
|
||||
favicons: [data.icon_url],
|
||||
};
|
||||
|
||||
setLinkPreviews(() => ({ [data.url]: preview }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.RelayHint) {
|
||||
const hints = JSON.parse(content.content);
|
||||
page.relayHints = { ...page.relayHints, ...hints };
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content?.kind === Kind.Zap) {
|
||||
const zapTag = content.tags.find(t => t[0] === 'description');
|
||||
|
||||
if (!zapTag) return;
|
||||
|
||||
const zapInfo = JSON.parse(zapTag[1] || '{}');
|
||||
|
||||
let amount = '0';
|
||||
|
||||
let bolt11Tag = content?.tags?.find(t => t[0] === 'bolt11');
|
||||
|
||||
if (bolt11Tag) {
|
||||
try {
|
||||
amount = `${parseBolt11(bolt11Tag[1]) || 0}`;
|
||||
} catch (e) {
|
||||
const amountTag = zapInfo.tags.find((t: string[]) => t[0] === 'amount');
|
||||
|
||||
amount = amountTag ? amountTag[1] : '0';
|
||||
}
|
||||
}
|
||||
|
||||
const eventId = (zapInfo.tags.find((t: string[]) => t[0] === 'e') || [])[1];
|
||||
|
||||
const zap: TopZap = {
|
||||
id: zapInfo.id,
|
||||
amount: parseInt(amount || '0'),
|
||||
pubkey: zapInfo.pubkey,
|
||||
message: zapInfo.content,
|
||||
eventId,
|
||||
};
|
||||
|
||||
if (page.topZaps[eventId] === undefined) {
|
||||
page.topZaps[eventId] = [{ ...zap }];
|
||||
return;
|
||||
}
|
||||
|
||||
if (page.topZaps[eventId].find(i => i.id === zap.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newZaps = [ ...page.topZaps[eventId], { ...zap }].sort((a, b) => b.amount - a.amount);
|
||||
|
||||
page.topZaps[eventId] = [ ...newZaps ];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.NoteQuoteStats) {
|
||||
const quoteStats = JSON.parse(content.content);
|
||||
|
||||
|
||||
// updateStore('quoteCount', () => quoteStats.count || 0);
|
||||
return;
|
||||
}
|
||||
|
@ -321,3 +321,13 @@ export const getMostZapped4h = (
|
||||
]},
|
||||
]));
|
||||
};
|
||||
|
||||
export const getReadsTopics = (
|
||||
subid: string,
|
||||
) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["get_reads_topics"]},
|
||||
]));
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { createStore } from "solid-js/store";
|
||||
import LinkPreview from "../components/LinkPreview/LinkPreview";
|
||||
import { addrRegex, appleMusicRegex, emojiRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, lnRegex, lnUnifiedRegex, mixCloudRegex, nostrNestsRegex, noteRegex, noteRegexLocal, profileRegex, profileRegexG, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, urlRegexG, wavlakeRegex, youtubeRegex } from "../constants";
|
||||
import { sendMessage, subscribeTo } from "../sockets";
|
||||
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalArticle, PrimalNote, SendNoteResult } from "../types/primal";
|
||||
import { EventCoordinate, MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalArticle, PrimalNote, SendNoteResult } from "../types/primal";
|
||||
import { npubToHex } from "./keys";
|
||||
import { logError, logInfo, logWarning } from "./logger";
|
||||
import { getMediaUrl as getMediaUrlDefault } from "./media";
|
||||
@ -343,7 +343,7 @@ export const sendArticleRepost = async (note: PrimalArticle, relays: Relay[], re
|
||||
kind: Kind.Repost,
|
||||
tags: [
|
||||
['e', note.id],
|
||||
['p', note.author.pubkey],
|
||||
['p', note.pubkey],
|
||||
],
|
||||
created_at: Math.floor((new Date()).getTime() / 1000),
|
||||
};
|
||||
@ -615,3 +615,12 @@ export const getParametrizedEvent = (pubkey: string, identifier: string, kind: n
|
||||
{cache: ["parametrized_replaceable_event", { pubkey, kind, identifier, extended_response: true }]},
|
||||
]));
|
||||
};
|
||||
|
||||
|
||||
export const getParametrizedEvents = (events: EventCoordinate[], subid: string) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["parametrized_replaceable_events", { events, extended_response: true }]},
|
||||
]));
|
||||
};
|
||||
|
@ -75,3 +75,11 @@ export const getScoredUsers = (user_pubkey: string | undefined, selector: string
|
||||
{cache: ['scored', { user_pubkey, selector }]},
|
||||
]));
|
||||
};
|
||||
|
||||
export const getRecomendedArticleIds = (subid: string) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ['get_recommended_reads']},
|
||||
]));
|
||||
};
|
||||
|
@ -491,7 +491,10 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
action={() => {}}
|
||||
/>
|
||||
|
||||
<PrimalMarkdown content={note.content || ''} readonly={true} />
|
||||
<PrimalMarkdown
|
||||
noteId={props.naddr}
|
||||
content={note.content || ''}
|
||||
readonly={true} />
|
||||
|
||||
<div class={styles.tags}>
|
||||
<For each={note.tags}>
|
||||
|
@ -35,6 +35,7 @@ import { useAppContext } from '../contexts/AppContext';
|
||||
import { useReadsContext } from '../contexts/ReadsContext';
|
||||
import ArticlePreview from '../components/ArticlePreview/ArticlePreview';
|
||||
import PageCaption from '../components/PageCaption/PageCaption';
|
||||
import ReadsSidebar from '../components/HomeSidebar/ReadsSidebar';
|
||||
|
||||
|
||||
const Home: Component = () => {
|
||||
@ -125,6 +126,10 @@ const Home: Component = () => {
|
||||
setNewPostAuthors(() => []);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
context?.actions.doSidebarSearch('')
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={styles.homeContent}>
|
||||
<PageTitle title={intl.formatMessage(branding)} />
|
||||
@ -137,7 +142,7 @@ const Home: Component = () => {
|
||||
<PageCaption title={intl.formatMessage(reads.pageTitle)} />
|
||||
|
||||
<StickySidebar>
|
||||
<HomeSidebar />
|
||||
<ReadsSidebar />
|
||||
</StickySidebar>
|
||||
|
||||
<Show
|
||||
|
2
src/types/primal.d.ts
vendored
2
src/types/primal.d.ts
vendored
@ -803,3 +803,5 @@ export type PageRange = {
|
||||
until: number,
|
||||
order_by: string,
|
||||
};
|
||||
|
||||
export type EventCoordinate = { pubkey: string, identifier: string, kind: number };
|
||||
|
Loading…
Reference in New Issue
Block a user