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 {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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} />}
|
||||
|
@ -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;
|
||||
}
|
||||
|
1
src/types/primal.d.ts
vendored
1
src/types/primal.d.ts
vendored
@ -533,6 +533,7 @@ export type PrimalArticle = {
|
||||
score: number,
|
||||
score24h: number,
|
||||
satszapped: number,
|
||||
client?: string,
|
||||
};
|
||||
|
||||
export type PrimalFeed = {
|
||||
|
Loading…
Reference in New Issue
Block a user