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 { .headingPicks {
@include heading(); @include heading();
font-weight: 600;
text-transform: capitalize; text-transform: capitalize;
height: fit-content; height: fit-content;
margin-bottom: 12px; margin-bottom: 12px;
padding-bottom: 0px; padding-bottom: 0px;
} }
.headingReads {
@include heading();
font-weight: 600;
text-transform: none;
height: fit-content;
margin-bottom: 12px;
padding-bottom: 0px;
}
.readsSidebar { .readsSidebar {
.section { .section {
@ -87,3 +97,56 @@
margin-right: 8px; 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<{ const ArticleFooter: Component<{
note: PrimalArticle, note: PrimalArticle,
wide?: boolean, size?: 'wide' | 'normal' | 'short',
id?: string, id?: string,
state: NoteReactionsState, state: NoteReactionsState,
updateState: SetStoreFunction<NoteReactionsState>, updateState: SetStoreFunction<NoteReactionsState>,
@ -49,6 +49,8 @@ const ArticleFooter: Component<{
let footerDiv: HTMLDivElement | undefined; let footerDiv: HTMLDivElement | undefined;
let repostMenu: HTMLDivElement | undefined; let repostMenu: HTMLDivElement | undefined;
const size = () => props.size ?? 'normal';
const repostMenuItems: MenuItem[] = [ const repostMenuItems: MenuItem[] = [
{ {
action: () => doRepost(), action: () => doRepost(),
@ -223,12 +225,17 @@ const ArticleFooter: Component<{
return; return;
} }
let newLeft = props.wide ? 15 : 13; let newLeft = 33;
let newTop = props.wide ? -6 : -6; let newTop = -6;
if (props.large) { if (size() === 'wide' && props.large) {
newLeft = 2; newLeft = 14;
newTop = -9; newTop = -10;
}
if (size() === 'short') {
newLeft = 14;
newTop = -6;
} }
medZapAnimation.style.left = `${newLeft}px`; medZapAnimation.style.left = `${newLeft}px`;
@ -319,7 +326,12 @@ const ArticleFooter: Component<{
} }
return ( 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}> <Show when={props.state.showZapAnim}>
<ZapAnimation <ZapAnimation

View File

@ -1,6 +1,6 @@
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { Kind } from "./constants"; import { Kind } from "./constants";
import { getEvents } from "./lib/feed"; import { getEvents, getUserArticleFeed } from "./lib/feed";
import { decodeIdentifier } from "./lib/keys"; import { decodeIdentifier } from "./lib/keys";
import { getParametrizedEvents, setLinkPreviews } from "./lib/notes"; import { getParametrizedEvents, setLinkPreviews } from "./lib/notes";
import { updateStore, store } from "./services/StoreService"; import { updateStore, store } from "./services/StoreService";
@ -549,6 +549,191 @@ export const fetchArticleThread = (pubkey: string | undefined, noteIds: string,
const quoteStats = JSON.parse(content.content); 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); // updateStore('quoteCount', () => quoteStats.count || 0);
return; return;
} }

View File

@ -57,10 +57,12 @@ export const getArticlesFeed = (user_pubkey: string | undefined, pubkey: string
let payload = { limit, [start]: until }; let payload = { limit, [start]: until };
if (pubkey && pubkey?.length > 0) { if (pubkey && pubkey?.length > 0) {
// @ts-ignore
payload.pubkey = pubkey; payload.pubkey = pubkey;
} }
if (user_pubkey) { if (user_pubkey) {
// @ts-ignore
payload.user_pubkey = user_pubkey; payload.user_pubkey = user_pubkey;
} }
@ -139,6 +141,34 @@ export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | un
{cache: ["feed", payload]}, {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 = ( export const getFutureUserFeed = (
user_pubkey: string | undefined, user_pubkey: string | undefined,

View File

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

View File

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

View File

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

View File

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