Article sidebar

This commit is contained in:
Bojan Mojsilovic 2024-06-04 15:54:35 +02:00
parent 1ce4ecd7da
commit cfa16f5964
9 changed files with 689 additions and 100 deletions

View File

@ -0,0 +1,92 @@
import { Component, createEffect, createSignal, For, onMount, Show } from 'solid-js';
import {
EventCoordinate,
PrimalArticle,
PrimalUser,
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, getUserArticleFeed } from '../../lib/feed';
import { fetchArticles, fetchRecomendedArticles } from '../../handleNotes';
import { getParametrizedEvent, getParametrizedEvents } from '../../lib/notes';
import { decodeIdentifier } from '../../lib/keys';
import ArticleShort from '../ArticlePreview/ArticleShort';
import { userName } from '../../stores/profile';
const ArticleSidebar: Component< { id?: string, user: PrimalUser, article: PrimalArticle } > = (props) => {
const account = useAccountContext();
const [recomended, setRecomended] = createStore<PrimalArticle[]>([]);
const [isFetchingArticles, setIsFetchingArticles] = createSignal(false);
const getArticles = async () => {
const subId = `article_recomended_${APP_ID}`;
setIsFetchingArticles(() => true);
const articles = await fetchRecomendedArticles(account?.publicKey, props.user.pubkey, 'authored', subId);
setRecomended(() => [...articles.filter(a => a.id !== props.article.id)]);
setIsFetchingArticles(() => false);
}
createEffect(() => {
if (account?.isKeyLookupDone && props.user) {
getArticles();
}
});
return (
<div id={props.id} class={styles.articleSidebar}>
<Show when={account?.isKeyLookupDone && props.article}>
<div class={styles.headingPicks}>
Total zaps
</div>
<div class={styles.section}>
<div class={styles.totalZaps}>
<span class={styles.totalZapsIcon} />
<span class={styles.amount}>26,450</span>
<span class={styles.unit}>sats</span>
</div>
</div>
<div class={styles.headingReads}>
More Reads from {userName(props.article.user)}
</div>
<Show
when={!isFetchingArticles()}
fallback={
<Loader />
}
>
<div class={styles.section}>
<For each={recomended}>
{(note) => <ArticleShort article={note} />}
</For>
</div>
</Show>
</Show>
</div>
);
}
export default hookForDev(ArticleSidebar);

View File

@ -59,12 +59,22 @@
.headingPicks {
@include heading();
font-weight: 600;
text-transform: capitalize;
height: fit-content;
margin-bottom: 12px;
padding-bottom: 0px;
}
.headingReads {
@include heading();
font-weight: 600;
text-transform: none;
height: fit-content;
margin-bottom: 12px;
padding-bottom: 0px;
}
.readsSidebar {
.section {
@ -87,3 +97,56 @@
margin-right: 8px;
}
}
.articleSidebar {
.section {
margin-bottom: 28px;
max-height: 526px;
overflow-y: scroll;
>a:last-child {
border-bottom: none;
}
.totalZaps {
display: flex;
align-items: flex-end;
gap: 4px;
.totalZapsIcon {
display: inline-block;
width: 18px;
height: 32px;
background: var(--active-zap);
-webkit-mask: url(../../assets/icons/feed_zap_fill_2.svg) no-repeat 0px 0 / 19px 32px;
mask: url(../../assets/icons/feed_zap_fill_2.svg) no-repeat 0px 0 / 19px 32px;
}
.amount {
color: var(--text-primary);
font-size: 28px;
font-weight: 700;
line-height: 32px;
}
.unit {
color: var(--text-primary);
font-size: 22px;
font-weight: 400;
line-height: 32px;
}
}
}
.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;
}
}

View File

@ -28,7 +28,7 @@ export const lottieDuration = () => zapMD.op * 1_000 / zapMD.fr;
const ArticleFooter: Component<{
note: PrimalArticle,
wide?: boolean,
size?: 'wide' | 'normal' | 'short',
id?: string,
state: NoteReactionsState,
updateState: SetStoreFunction<NoteReactionsState>,
@ -49,6 +49,8 @@ const ArticleFooter: Component<{
let footerDiv: HTMLDivElement | undefined;
let repostMenu: HTMLDivElement | undefined;
const size = () => props.size ?? 'normal';
const repostMenuItems: MenuItem[] = [
{
action: () => doRepost(),
@ -223,12 +225,17 @@ const ArticleFooter: Component<{
return;
}
let newLeft = props.wide ? 15 : 13;
let newTop = props.wide ? -6 : -6;
let newLeft = 33;
let newTop = -6;
if (props.large) {
newLeft = 2;
newTop = -9;
if (size() === 'wide' && props.large) {
newLeft = 14;
newTop = -10;
}
if (size() === 'short') {
newLeft = 14;
newTop = -6;
}
medZapAnimation.style.left = `${newLeft}px`;
@ -319,7 +326,12 @@ const ArticleFooter: Component<{
}
return (
<div id={props.id} class={`${styles.footer} ${props.wide ? styles.wide : ''}`} ref={footerDiv} onClick={(e) => {e.preventDefault();}}>
<div
id={props.id}
class={`${styles.footer} ${styles[size()]}`}
ref={footerDiv}
onClick={(e) => {e.preventDefault();}}
>
<Show when={props.state.showZapAnim}>
<ZapAnimation

View File

@ -1,6 +1,6 @@
import { nip19 } from "nostr-tools";
import { Kind } from "./constants";
import { getEvents } from "./lib/feed";
import { getEvents, getUserArticleFeed } from "./lib/feed";
import { decodeIdentifier } from "./lib/keys";
import { getParametrizedEvents, setLinkPreviews } from "./lib/notes";
import { updateStore, store } from "./services/StoreService";
@ -549,6 +549,191 @@ export const fetchArticleThread = (pubkey: string | undefined, noteIds: string,
const quoteStats = JSON.parse(content.content);
// updateStore('quoteCount', () => quoteStats.count || 0);
return;
}
};
});
};
export const fetchRecomendedArticles = (userPubkey: string | undefined, pubkey: string | undefined, type: 'authored' | 'replies' | 'bookmarks', 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,
wordCount: {},
}
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);
}
});
getUserArticleFeed(userPubkey, pubkey, subId, type);
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.WordCount) {
const count = JSON.parse(content.content) as { event_id: string, words: number };
if (!page.wordCount) {
page.wordCount = {};
}
page.wordCount[count.event_id] = count.words
return;
}
if (content.kind === Kind.NoteQuoteStats) {
const quoteStats = JSON.parse(content.content);
// updateStore('quoteCount', () => quoteStats.count || 0);
return;
}

View File

@ -57,10 +57,12 @@ export const getArticlesFeed = (user_pubkey: string | undefined, pubkey: string
let payload = { limit, [start]: until };
if (pubkey && pubkey?.length > 0) {
// @ts-ignore
payload.pubkey = pubkey;
}
if (user_pubkey) {
// @ts-ignore
payload.user_pubkey = user_pubkey;
}
@ -139,6 +141,34 @@ export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | un
{cache: ["feed", payload]},
]));
}
export const getUserArticleFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, notes: 'authored' | 'replies' | 'bookmarks', until = 0, limit = 20, offset = 0) => {
if (!pubkey) {
return;
}
let payload: {
pubkey: string,
limit: number,
notes: 'authored' | 'replies' | 'bookmarks',
user_pubkey?: string,
until?: number,
offset?: number,
} = { pubkey, limit, notes } ;
if (user_pubkey) {
payload.user_pubkey = user_pubkey;
}
if (until > 0) payload.until = until;
if (offset > 0) payload.offset = offset;
sendMessage(JSON.stringify([
"REQ",
subid,
{cache: ["long_form_content_feed", payload]},
]));
}
export const getFutureUserFeed = (
user_pubkey: string | undefined,

View File

@ -45,6 +45,7 @@
justify-content: flex-start;
align-items: center;
gap: 4px;
max-width: 80%;
.time {
color: var(--text-tertiary);
@ -59,6 +60,10 @@
color: var(--text-tertiary);
font-size: 14px;
font-weight: 700;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@ -74,7 +79,7 @@
flex-direction: column;
gap: 20px;
position: relative;
margin-bottom: 48px;
margin-bottom: 22px;
margin-inline: 20px;
.title {
@ -145,7 +150,8 @@
padding: 6px 10px;
border-radius: 12px;
width: fit-content;
margin: 4px;
margin-block: 4px;
margin-right: 6px;
}
}
}

View File

@ -1,6 +1,6 @@
import { useIntl } from "@cookbook/solid-intl";
import { useParams } from "@solidjs/router";
import { Component, createEffect, createSignal, For, onCleanup, onMount, Show } from "solid-js";
import { batch, Component, createEffect, createSignal, For, onCleanup, onMount, Show } from "solid-js";
import { createStore } from "solid-js/store";
import { APP_ID } from "../App";
import { Kind } from "../constants";
@ -12,8 +12,8 @@ import { SolidMarkdown } from "solid-markdown";
import styles from './Longform.module.scss';
import Loader from "../components/Loader/Loader";
import { FeedPage, NostrEventContent, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalNote, PrimalUser, TopZap } from "../types/primal";
import { getUserProfileInfo } from "../lib/profile";
import { FeedPage, NostrEventContent, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalArticle, PrimalNote, PrimalUser, TopZap, ZapOption } from "../types/primal";
import { getUserProfileInfo, getUserProfiles } from "../lib/profile";
import { convertToUser, nip05Verification, userName } from "../stores/profile";
import Avatar from "../components/Avatar/Avatar";
import { shortDate } from "../lib/dates";
@ -25,7 +25,7 @@ import { full as mdEmoji } from 'markdown-it-emoji';
import PrimalMarkdown from "../components/PrimalMarkdown/PrimalMarkdown";
import NoteTopZaps from "../components/Note/NoteTopZaps";
import { parseBolt11 } from "../utils";
import { parseBolt11, uuidv4 } from "../utils";
import Note, { NoteReactionsState } from "../components/Note/Note";
import NoteFooter from "../components/Note/NoteFooter/NoteFooter";
import { getArticleThread, getThread } from "../lib/feed";
@ -33,12 +33,19 @@ import PhotoSwipeLightbox from "photoswipe/lightbox";
import NoteImage from "../components/NoteImage/NoteImage";
import { nip19 } from "nostr-tools";
import { saveNotes } from "../services/StoreService";
import { sortByRecency, convertToNotes } from "../stores/note";
import { sortByRecency, convertToNotes, convertToArticles } from "../stores/note";
import { tableNodeTypes } from "@milkdown/prose/tables";
import VerificationCheck from "../components/VerificationCheck/VerificationCheck";
import BookmarkArticle from "../components/BookmarkNote/BookmarkArticle";
import NoteContextTrigger from "../components/Note/NoteContextTrigger";
import { useAppContext } from "../contexts/AppContext";
import { CustomZapInfo, useAppContext } from "../contexts/AppContext";
import ArticleFooter from "../components/Note/NoteFooter/ArticleFooter";
import { thread } from "../translations";
import { useThreadContext } from "../contexts/ThreadContext";
import Wormhole from "../components/Wormhole/Wormhole";
import Search from "../components/Search/Search";
import ArticleSidebar from "../components/HomeSidebar/ArticleSidebar";
import ReplyToNote from "../components/ReplyToNote/ReplyToNote";
export type LongFormData = {
title: string,
@ -54,6 +61,7 @@ export type LongFormData = {
};
export type LongformThreadStore = {
article: PrimalArticle | undefined,
page: FeedPage,
replies: PrimalNote[],
users: PrimalUser[],
@ -75,6 +83,7 @@ const emptyArticle = {
};
const emptyStore: LongformThreadStore = {
article: undefined,
replies: [],
page: {
messages: [],
@ -286,19 +295,22 @@ Term 2 with *inline markup*
const Longform: Component< { naddr: string } > = (props) => {
const account = useAccountContext();
const app = useAppContext();
const thread = useThreadContext();
const params = useParams();
const intl = useIntl();
const [article, setArticle] = createStore<LongFormData>({...emptyArticle});
// const [article, setArticle] = createStore<LongFormData>({...emptyArticle});
const [store, updateStore] = createStore<LongformThreadStore>({ ...emptyStore })
const [pubkey, setPubkey] = createSignal<string>('');
// const [pubkey, setPubkey] = createSignal<string>('');
// @ts-ignore
const [author, setAuthor] = createStore<PrimalUser>();
const naddr = () => props.naddr;
let latestTopZap: string = '';
let latestTopZapFeed: string = '';
let articleContextMenu: HTMLDivElement | undefined;
const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
@ -340,8 +352,149 @@ const Longform: Component< { naddr: string } > = (props) => {
fetchArticle();
});
const onConfirmZap = (zapOption: ZapOption) => {
app?.actions.closeCustomZapModal();
batch(() => {
updateReactionsState('zappedAmount', () => zapOption.amount || 0);
updateReactionsState('satsZapped', (z) => z + (zapOption.amount || 0));
updateReactionsState('zapped', () => true);
updateReactionsState('showZapAnim', () => true)
});
addTopZap(zapOption);
addTopZapFeed(zapOption)
};
const onSuccessZap = (zapOption: ZapOption) => {
app?.actions.closeCustomZapModal();
app?.actions.resetCustomZap();
const pubkey = account?.publicKey;
if (!pubkey) return;
batch(() => {
updateReactionsState('zapCount', (z) => z + 1);
updateReactionsState('isZapping', () => false);
updateReactionsState('showZapAnim', () => false);
updateReactionsState('hideZapIcon', () => false);
updateReactionsState('zapped', () => true);
});
};
const onFailZap = (zapOption: ZapOption) => {
app?.actions.closeCustomZapModal();
app?.actions.resetCustomZap();
batch(() => {
updateReactionsState('zappedAmount', () => -(zapOption.amount || 0));
updateReactionsState('satsZapped', (z) => z - (zapOption.amount || 0));
updateReactionsState('isZapping', () => false);
updateReactionsState('showZapAnim', () => false);
updateReactionsState('hideZapIcon', () => false);
updateReactionsState('zapped', () => store.article ? store.article.noteActions.zapped : false);
});
removeTopZap(zapOption);
removeTopZapFeed(zapOption);
};
const onCancelZap = (zapOption: ZapOption) => {
app?.actions.closeCustomZapModal();
app?.actions.resetCustomZap();
batch(() => {
updateReactionsState('zappedAmount', () => -(zapOption.amount || 0));
updateReactionsState('satsZapped', (z) => z - (zapOption.amount || 0));
updateReactionsState('isZapping', () => false);
updateReactionsState('showZapAnim', () => false);
updateReactionsState('hideZapIcon', () => false);
updateReactionsState('zapped', () => store.article ? store.article.noteActions.zapped : false);
});
removeTopZap(zapOption);
removeTopZapFeed(zapOption);
};
const addTopZap = (zapOption: ZapOption) => {
const pubkey = account?.publicKey;
if (!pubkey || !store.article) return;
const oldZaps = [ ...reactionsState.topZaps ];
latestTopZap = uuidv4() as string;
const newZap = {
amount: zapOption.amount || 0,
message: zapOption.message || '',
pubkey,
eventId: store.article.id,
id: latestTopZap,
};
if (!store.users.find((u) => u.pubkey === pubkey)) {
const subId = `article_pk_${APP_ID}`;
const unsub = subscribeTo(subId, (type, _, content) =>{
if (type === 'EOSE') {
unsub();
savePage(store.page);
return;
}
if (type === 'EVENT') {
content && updatePage(content);
}
});
getUserProfiles([pubkey], subId);
}
const zaps = [ ...oldZaps, { ...newZap }].sort((a, b) => b.amount - a.amount);
updateReactionsState('topZaps', () => [...zaps]);
};
const removeTopZap = (zapOption: ZapOption) => {
const zaps = reactionsState.topZaps.filter(z => z.id !== latestTopZap);
updateReactionsState('topZaps', () => [...zaps]);
};
const addTopZapFeed = (zapOption: ZapOption) => {
const pubkey = account?.publicKey;
if (!pubkey || !store.article) return;
const oldZaps = [ ...reactionsState.topZapsFeed ];
latestTopZapFeed = uuidv4() as string;
const newZap = {
amount: zapOption.amount || 0,
message: zapOption.message || '',
pubkey,
eventId: store.article.id,
id: latestTopZapFeed,
};
const zaps = [ ...oldZaps, { ...newZap }].sort((a, b) => b.amount - a.amount).slice(0, 4);
updateReactionsState('topZapsFeed', () => [...zaps]);
}
const removeTopZapFeed = (zapOption: ZapOption) => {
const zaps = reactionsState.topZapsFeed.filter(z => z.id !== latestTopZapFeed);
updateReactionsState('topZapsFeed', () => [...zaps]);
};
const customZapInfo: () => CustomZapInfo = () => ({
note: store.article,
onConfirm: onConfirmZap,
onSuccess: onSuccessZap,
onFail: onFailZap,
onCancel: onCancelZap,
});
const clearArticle = () => {
setArticle(() => ({ ...emptyArticle }));
// setArticle(() => ({ ...emptyArticle }));
updateStore(() => ({ ...emptyStore }));
};
@ -394,57 +547,57 @@ const Longform: Component< { naddr: string } > = (props) => {
return;
}
if (content.kind === Kind.LongForm) {
// if (content.kind === Kind.LongForm) {
let n: LongFormData = {
title: '',
summary: '',
image: '',
tags: [],
published: content.created_at || 0,
content: content.content,
author: content.pubkey,
topZaps: [],
id: content.id,
client: '',
}
// let n: LongFormData = {
// title: '',
// summary: '',
// image: '',
// tags: [],
// published: content.created_at || 0,
// content: content.content,
// author: content.pubkey,
// topZaps: [],
// id: content.id,
// client: '',
// }
content.tags.forEach(tag => {
switch (tag[0]) {
case 't':
n.tags.push(tag[1]);
break;
case 'title':
n.title = tag[1];
break;
case 'summary':
n.summary = tag[1];
break;
case 'image':
n.image = tag[1];
break;
case 'published':
n.published = parseInt(tag[1]);
break;
case 'content':
n.content = tag[1];
break;
case 'author':
n.author = tag[1];
break;
case 'client':
n.client = tag[1];
break;
default:
break;
}
});
// content.tags.forEach(tag => {
// switch (tag[0]) {
// case 't':
// n.tags.push(tag[1]);
// break;
// case 'title':
// n.title = tag[1];
// break;
// case 'summary':
// n.summary = tag[1];
// break;
// case 'image':
// n.image = tag[1];
// break;
// case 'published':
// n.published = parseInt(tag[1]);
// break;
// case 'content':
// n.content = tag[1];
// break;
// case 'author':
// n.author = tag[1];
// break;
// case 'client':
// n.client = tag[1];
// break;
// default:
// break;
// }
// });
setArticle(n);
return;
}
// setArticle(n);
// return;
// }
if ([Kind.Text, Kind.Repost].includes(content.kind)) {
if ([Kind.LongForm, Kind.Text, Kind.Repost].includes(content.kind)) {
const message = content as NostrNoteContent;
if (store.lastReply?.noteId !== nip19.noteEncode(message.id)) {
@ -544,10 +697,10 @@ const Longform: Component< { naddr: string } > = (props) => {
eventId,
};
if (article.id === zap.eventId && !article.topZaps.find(i => i.id === zap.id)) {
const newZaps = [ ...article.topZaps, { ...zap }].sort((a, b) => b.amount - a.amount);
setArticle('topZaps', (zaps) => [ ...newZaps ]);
}
// if (article.id === zap.eventId && !article.topZaps.find(i => i.id === zap.id)) {
// const newZaps = [ ...article.topZaps, { ...zap }].sort((a, b) => b.amount - a.amount);
// setArticle('topZaps', (zaps) => [ ...newZaps ]);
// }
const oldZaps = store.page.topZaps[eventId];
@ -569,27 +722,40 @@ const Longform: Component< { naddr: string } > = (props) => {
};
const savePage = (page: FeedPage) => {
const newPosts = sortByRecency(convertToNotes(page, page.topZaps));
const pageWithNotes = {
...page,
messages: page.messages.filter(m => m.kind === Kind.Text)
}
const users = Object.values(page.users).map(convertToUser);
const replies = sortByRecency(convertToNotes(pageWithNotes, pageWithNotes.topZaps));
const articles = convertToArticles(page, page.topZaps);
const article = articles.find(a => a.noteId === naddr());
updateStore('users', () => [ ...users ]);
saveNotes(newPosts);
updateStore('replies', (notes) => [ ...notes, ...replies ]);
const a = users.find(u => u.pubkey === article.author);
updateStore('article', () => ({ ...article }));
if (a) {
setAuthor(() => ({ ...a }));
}
};
const saveNotes = (newNotes: PrimalNote[], scope?: 'future') => {
updateStore('replies', (notes) => [ ...notes, ...newNotes ]);
updateStore('isFetching', () => false);
// saveNotes(replies);
// const a = users.find(u => u.pubkey === article.author);
// if (a) {
// setAuthor(() => ({ ...a }));
// }
};
// const saveNotes = (newNotes: PrimalNote[], scope?: 'future') => {
// };
const openReactionModal = (openOn = 'likes') => {
app?.actions.openReactionModal(article.id, {
if (!store.article) return;
app?.actions.openReactionModal(store.article.id, {
likes: reactionsState.likes,
zaps: reactionsState.zapCount,
reposts: reactionsState.reposts,
@ -599,18 +765,31 @@ const Longform: Component< { naddr: string } > = (props) => {
};
const onContextMenuTrigger = () => {
// app?.actions.openContextMenu(
// article,
// articleContextMenu?.getBoundingClientRect(),
// () => {
// app?.actions.openCustomZapModal(customZapInfo());
// },
// openReactionModal,
// );
if (!store.article) return;
app?.actions.openContextMenu(
store.article,
articleContextMenu?.getBoundingClientRect(),
() => {
app?.actions.openCustomZapModal(customZapInfo());
},
openReactionModal,
);
}
return (
<>
<Wormhole
to="search_section"
>
<Search />
</Wormhole>
<Wormhole to='right_sidebar'>
<ArticleSidebar
user={store.article?.user}
article={store.article}
/>
</Wormhole>
<div class={styles.header}>
<div class={styles.author}>
<Show when={author}>
@ -633,17 +812,17 @@ const Longform: Component< { naddr: string } > = (props) => {
<div class={styles.topBar}>
<div class={styles.left}>
<div class={styles.time}>
{shortDate(article.published)}
{shortDate(store.article?.published)}
</div>
<Show when={article.client.length > 0}>
<Show when={store.article?.client}>
<div class={styles.client}>
via {article.client}
via {store.article?.client}
</div>
</Show>
</div>
<div class={styles.right}>
<BookmarkArticle article={article} />
<BookmarkArticle note={store.article} />
<NoteContextTrigger
ref={articleContextMenu}
onClick={onContextMenuTrigger}
@ -653,28 +832,28 @@ const Longform: Component< { naddr: string } > = (props) => {
<div id={`read_${naddr()}`} class={styles.longform}>
<Show
when={article.content.length > 0}
when={store.article}
fallback={<Loader />}
>
<div class={styles.title}>
{article.title}
{store.article?.title}
</div>
<NoteImage
class={`${styles.image} hero_image_${naddr()}`}
src={article.image}
src={store.article?.image}
width={640}
/>
<div class={styles.summary}>
<div class={styles.border}></div>
<div class={styles.text}>
{article.summary}
{store.article?.summary}
</div>
</div>
<NoteTopZaps
topZaps={article.topZaps}
topZaps={store.article?.topZaps}
zapCount={reactionsState.zapCount}
users={store.users}
action={() => {}}
@ -682,11 +861,11 @@ const Longform: Component< { naddr: string } > = (props) => {
<PrimalMarkdown
noteId={props.naddr}
content={article.content || ''}
content={store.article?.content || ''}
readonly={true} />
<div class={styles.tags}>
<For each={article.tags}>
<For each={store.article?.tags}>
{tag => (
<div class={styles.tag}>
{tag}
@ -699,8 +878,26 @@ const Longform: Component< { naddr: string } > = (props) => {
children={note.content || ''}
/>
</div> */}
<div class={styles.footer}>
<ArticleFooter
note={store.article}
state={reactionsState}
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
onZapAnim={addTopZapFeed}
/>
</div>
</Show>
</div>
<Show when={store.article}>
<ReplyToNote
note={store.article}
onNotePosted={() => {}}
/>
</Show>
<div>
<For each={store.replies}>
{reply => <Note note={reply} />}

View File

@ -505,6 +505,9 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
case 'published':
article.published = parseInt(tag[1]);
break;
case 'client':
article.client = tag[1];
break;
default:
break;
}

View File

@ -533,6 +533,7 @@ export type PrimalArticle = {
score: number,
score24h: number,
satszapped: number,
client?: string,
};
export type PrimalFeed = {