mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Article sidebar
This commit is contained in:
parent
1ce4ecd7da
commit
cfa16f5964
92
src/components/HomeSidebar/ArticleSidebar.tsx
Normal file
92
src/components/HomeSidebar/ArticleSidebar.tsx
Normal 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);
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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} />}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
1
src/types/primal.d.ts
vendored
1
src/types/primal.d.ts
vendored
@ -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 = {
|
||||||
|
Loading…
Reference in New Issue
Block a user