mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Redo long-form note view
This commit is contained in:
parent
622dad8ecc
commit
0081870198
@ -4,13 +4,14 @@ import { useThreadContext } from "../../contexts/ThreadContext";
|
||||
import Avatar from "../Avatar/Avatar";
|
||||
import { TransitionGroup } from 'solid-transition-group';
|
||||
import styles from "./Note.module.scss";
|
||||
import { TopZap } from "../../types/primal";
|
||||
import { PrimalUser, TopZap } from "../../types/primal";
|
||||
|
||||
const NoteTopZaps: Component<{
|
||||
topZaps: TopZap[],
|
||||
zapCount: number,
|
||||
action: () => void,
|
||||
id?: string,
|
||||
users?: PrimalUser[]
|
||||
}> = (props) => {
|
||||
|
||||
const threadContext = useThreadContext();
|
||||
@ -42,7 +43,7 @@ const NoteTopZaps: Component<{
|
||||
}
|
||||
|
||||
const zapSender = (zap: TopZap) => {
|
||||
return threadContext?.users.find(u => u.pubkey === zap.pubkey);
|
||||
return (props.users || threadContext?.users || []).find(u => u.pubkey === zap.pubkey);
|
||||
};
|
||||
return (
|
||||
|
||||
|
@ -145,7 +145,6 @@ body::after{
|
||||
url(./assets/icons/nav/settings_selected.svg)
|
||||
url(./assets/icons/nav/long.svg)
|
||||
url(./assets/icons/nav/long_selected.svg)
|
||||
url(./assets/images/reads_image.svg)
|
||||
url(./assets/images/reads_image_dark.png)
|
||||
url(./assets/images/reads_image_light.png);
|
||||
}
|
||||
|
@ -11,18 +11,62 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.userName {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nip05 {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.topBar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 18px;
|
||||
margin-top: 18px;
|
||||
padding-inline: 20px;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.client {
|
||||
&::before {
|
||||
content: '• ';
|
||||
}
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.longform {
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { useIntl } from "@cookbook/solid-intl";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { Component, createEffect, createSignal, For, onMount, Show } from "solid-js";
|
||||
import { 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";
|
||||
import { useAccountContext } from "../contexts/AccountContext";
|
||||
import { decodeIdentifier } from "../lib/keys";
|
||||
import { getParametrizedEvent } from "../lib/notes";
|
||||
import { getParametrizedEvent, setLinkPreviews } from "../lib/notes";
|
||||
import { subscribeTo } from "../sockets";
|
||||
import { SolidMarkdown } from "solid-markdown";
|
||||
|
||||
import styles from './Longform.module.scss';
|
||||
import Loader from "../components/Loader/Loader";
|
||||
import { NostrUserContent, PrimalUser, TopZap } from "../types/primal";
|
||||
import { FeedPage, NostrEventContent, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalNote, PrimalUser, TopZap } from "../types/primal";
|
||||
import { getUserProfileInfo } from "../lib/profile";
|
||||
import { convertToUser, userName } from "../stores/profile";
|
||||
import { convertToUser, nip05Verification, userName } from "../stores/profile";
|
||||
import Avatar from "../components/Avatar/Avatar";
|
||||
import { shortDate } from "../lib/dates";
|
||||
|
||||
@ -26,11 +26,19 @@ 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 { NoteReactionsState } from "../components/Note/Note";
|
||||
import Note, { NoteReactionsState } from "../components/Note/Note";
|
||||
import NoteFooter from "../components/Note/NoteFooter/NoteFooter";
|
||||
import { getArticleThread, getThread } from "../lib/feed";
|
||||
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 { 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";
|
||||
|
||||
export type LongFormData = {
|
||||
title: string,
|
||||
@ -41,9 +49,19 @@ export type LongFormData = {
|
||||
content: string,
|
||||
author: string,
|
||||
topZaps: TopZap[],
|
||||
id: string,
|
||||
client: string,
|
||||
};
|
||||
|
||||
const emptyLongNote = {
|
||||
export type LongformThreadStore = {
|
||||
page: FeedPage,
|
||||
replies: PrimalNote[],
|
||||
users: PrimalUser[],
|
||||
isFetching: boolean,
|
||||
lastReply: PrimalNote | undefined,
|
||||
}
|
||||
|
||||
const emptyArticle = {
|
||||
title: '',
|
||||
summary: '',
|
||||
image: '',
|
||||
@ -52,6 +70,24 @@ const emptyLongNote = {
|
||||
content: '',
|
||||
author: '',
|
||||
topZaps: [],
|
||||
id: '',
|
||||
client: '',
|
||||
};
|
||||
|
||||
const emptyStore: LongformThreadStore = {
|
||||
replies: [],
|
||||
page: {
|
||||
messages: [],
|
||||
users: {},
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
topZaps: {},
|
||||
wordCount: {},
|
||||
},
|
||||
users: [],
|
||||
isFetching: false,
|
||||
lastReply: undefined,
|
||||
}
|
||||
|
||||
const test = `
|
||||
@ -249,10 +285,12 @@ Term 2 with *inline markup*
|
||||
|
||||
const Longform: Component< { naddr: string } > = (props) => {
|
||||
const account = useAccountContext();
|
||||
const app = useAppContext();
|
||||
const params = useParams();
|
||||
const intl = useIntl();
|
||||
|
||||
const [note, setNote] = createStore<LongFormData>({...emptyLongNote});
|
||||
const [article, setArticle] = createStore<LongFormData>({...emptyArticle});
|
||||
const [store, updateStore] = createStore<LongformThreadStore>({ ...emptyStore })
|
||||
|
||||
const [pubkey, setPubkey] = createSignal<string>('');
|
||||
|
||||
@ -261,6 +299,8 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
|
||||
const naddr = () => props.naddr;
|
||||
|
||||
let articleContextMenu: HTMLDivElement | undefined;
|
||||
|
||||
const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
|
||||
likes: 0,
|
||||
liked: false,
|
||||
@ -296,80 +336,66 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
|
||||
onMount(() => {
|
||||
lightbox.init();
|
||||
clearArticle();
|
||||
fetchArticle();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!pubkey()) {
|
||||
return;
|
||||
}
|
||||
const clearArticle = () => {
|
||||
setArticle(() => ({ ...emptyArticle }));
|
||||
updateStore(() => ({ ...emptyStore }));
|
||||
};
|
||||
|
||||
const subId = `author_${naddr()}_${APP_ID}`;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, subId, content) =>{
|
||||
if (type === 'EOSE') {
|
||||
unsub();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(content.kind === Kind.Metadata) {
|
||||
const userContent = content as NostrUserContent;
|
||||
|
||||
const user = convertToUser(userContent);
|
||||
|
||||
setAuthor(() => ({ ...user }));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
getUserProfileInfo(pubkey(), account?.publicKey, subId);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (naddr() === 'naddr1_test') {
|
||||
|
||||
setNote(() => ({
|
||||
title: 'Test Long-Form Note',
|
||||
summary: 'This is a markdown test to show all elements of the markdown',
|
||||
image: '',
|
||||
tags: ['test', 'markdown', 'demo'],
|
||||
published: (new Date()).getTime() / 1_000,
|
||||
content: test,
|
||||
author: account?.publicKey,
|
||||
topZaps: [],
|
||||
}));
|
||||
|
||||
setPubkey(() => note.author);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof naddr() === 'string' && naddr().startsWith('naddr')) {
|
||||
const fetchArticle = () => {
|
||||
const decoded = decodeIdentifier(naddr());
|
||||
|
||||
const { pubkey, identifier, kind } = decoded.data;
|
||||
|
||||
if (kind !== Kind.LongForm) return;
|
||||
|
||||
const subId = `naddr_${naddr()}_${APP_ID}`;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, subId, content) =>{
|
||||
if (type === 'EOSE') {
|
||||
unsub();
|
||||
savePage(store.page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (!content) {
|
||||
content && updatePage(content);
|
||||
}
|
||||
});
|
||||
|
||||
updateStore('isFetching', () => true);
|
||||
|
||||
updateStore('page', () => ({
|
||||
messages: [],
|
||||
users: {},
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
topZaps: {},
|
||||
wordCount: {},
|
||||
}));
|
||||
|
||||
getArticleThread(account?.publicKey, pubkey, identifier, kind, subId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const updatePage = (content: NostrEventContent) => {
|
||||
if (content.kind === Kind.Metadata) {
|
||||
const user = content as NostrUserContent;
|
||||
|
||||
updateStore('page', 'users',
|
||||
(usrs) => ({ ...usrs, [user.pubkey]: { ...user } })
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.LongForm) {
|
||||
|
||||
setPubkey(() => content.pubkey);
|
||||
|
||||
let n: LongFormData = {
|
||||
title: '',
|
||||
summary: '',
|
||||
@ -378,7 +404,9 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
published: content.created_at || 0,
|
||||
content: content.content,
|
||||
author: content.pubkey,
|
||||
topZaps: note.topZaps || [],
|
||||
topZaps: [],
|
||||
id: content.id,
|
||||
client: '',
|
||||
}
|
||||
|
||||
content.tags.forEach(tag => {
|
||||
@ -404,14 +432,86 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
case 'author':
|
||||
n.author = tag[1];
|
||||
break;
|
||||
case 'client':
|
||||
n.client = tag[1];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
setNote(() => ({...n}));
|
||||
setArticle(n);
|
||||
return;
|
||||
}
|
||||
|
||||
if ([Kind.Text, Kind.Repost].includes(content.kind)) {
|
||||
const message = content as NostrNoteContent;
|
||||
|
||||
if (store.lastReply?.noteId !== nip19.noteEncode(message.id)) {
|
||||
updateStore('page', 'messages',
|
||||
(msgs) => [ ...msgs, { ...message }]
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.NoteStats) {
|
||||
const statistic = content as NostrStatsContent;
|
||||
const stat = JSON.parse(statistic.content);
|
||||
|
||||
updateStore('page', 'postStats',
|
||||
(stats) => ({ ...stats, [stat.event_id]: { ...stat } })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.Mentions) {
|
||||
const mentionContent = content as NostrMentionContent;
|
||||
const mention = JSON.parse(mentionContent.content);
|
||||
|
||||
updateStore('page', 'mentions',
|
||||
(mentions) => ({ ...mentions, [mention.id]: { ...mention } })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.NoteActions) {
|
||||
const noteActionContent = content as NostrNoteActionsContent;
|
||||
const noteActions = JSON.parse(noteActionContent.content) as NoteActions;
|
||||
|
||||
updateStore('page', 'noteActions',
|
||||
(actions) => ({ ...actions, [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);
|
||||
updateStore('page', 'relayHints', (rh) => ({ ...rh, ...hints }));
|
||||
}
|
||||
|
||||
if (content?.kind === Kind.Zap) {
|
||||
const zapTag = content.tags.find(t => t[0] === 'description');
|
||||
@ -444,10 +544,15 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
eventId,
|
||||
};
|
||||
|
||||
const oldZaps = note.topZaps;
|
||||
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 (!oldZaps || oldZaps.length === 0) {
|
||||
setNote((n) => ({ ...n, topZaps: [{ ...zap }]}));
|
||||
const oldZaps = store.page.topZaps[eventId];
|
||||
|
||||
if (oldZaps === undefined) {
|
||||
updateStore('page', 'topZaps', () => ({ [eventId]: [{ ...zap }]}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -457,17 +562,52 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
|
||||
const newZaps = [ ...oldZaps, { ...zap }].sort((a, b) => b.amount - a.amount);
|
||||
|
||||
setNote((n) => ({ ...n, topZaps: [...newZaps]}));
|
||||
updateStore('page', 'topZaps', eventId, () => [ ...newZaps ]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// getThread(account?.publicKey, naddr, subId)
|
||||
getArticleThread(account?.publicKey, pubkey, identifier, kind, subId);
|
||||
const savePage = (page: FeedPage) => {
|
||||
const newPosts = sortByRecency(convertToNotes(page, page.topZaps));
|
||||
const users = Object.values(page.users).map(convertToUser);
|
||||
|
||||
updateStore('users', () => [ ...users ]);
|
||||
|
||||
saveNotes(newPosts);
|
||||
|
||||
const a = users.find(u => u.pubkey === article.author);
|
||||
|
||||
if (a) {
|
||||
setAuthor(() => ({ ...a }));
|
||||
}
|
||||
};
|
||||
|
||||
const saveNotes = (newNotes: PrimalNote[], scope?: 'future') => {
|
||||
updateStore('replies', (notes) => [ ...notes, ...newNotes ]);
|
||||
updateStore('isFetching', () => false);
|
||||
};
|
||||
|
||||
const openReactionModal = (openOn = 'likes') => {
|
||||
app?.actions.openReactionModal(article.id, {
|
||||
likes: reactionsState.likes,
|
||||
zaps: reactionsState.zapCount,
|
||||
reposts: reactionsState.reposts,
|
||||
quotes: reactionsState.quoteCount,
|
||||
openOn,
|
||||
});
|
||||
};
|
||||
|
||||
const onContextMenuTrigger = () => {
|
||||
// app?.actions.openContextMenu(
|
||||
// article,
|
||||
// articleContextMenu?.getBoundingClientRect(),
|
||||
// () => {
|
||||
// app?.actions.openCustomZapModal(customZapInfo());
|
||||
// },
|
||||
// openReactionModal,
|
||||
// );
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -475,50 +615,78 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
<div class={styles.author}>
|
||||
<Show when={author}>
|
||||
<Avatar user={author} size="xs" />
|
||||
<div class={styles.userInfo}>
|
||||
<div class={styles.userName}>
|
||||
{userName(author)}
|
||||
<VerificationCheck user={author} />
|
||||
</div>
|
||||
<Show when={author.nip05}>
|
||||
<div class={styles.nip05}>
|
||||
{nip05Verification(author)}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.topBar}>
|
||||
<div class={styles.left}>
|
||||
<div class={styles.time}>
|
||||
{shortDate(note.published)}
|
||||
{shortDate(article.published)}
|
||||
</div>
|
||||
<Show when={article.client.length > 0}>
|
||||
<div class={styles.client}>
|
||||
via {article.client}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={styles.right}>
|
||||
<BookmarkArticle article={article} />
|
||||
<NoteContextTrigger
|
||||
ref={articleContextMenu}
|
||||
onClick={onContextMenuTrigger}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id={`read_${naddr()}`} class={styles.longform}>
|
||||
<Show
|
||||
when={note.content.length > 0}
|
||||
when={article.content.length > 0}
|
||||
fallback={<Loader />}
|
||||
>
|
||||
<div class={styles.title}>
|
||||
{note.title}
|
||||
{article.title}
|
||||
</div>
|
||||
|
||||
<NoteImage
|
||||
class={`${styles.image} hero_image_${naddr()}`}
|
||||
src={note.image}
|
||||
src={article.image}
|
||||
width={640}
|
||||
/>
|
||||
|
||||
<div class={styles.summary}>
|
||||
<div class={styles.border}></div>
|
||||
<div class={styles.text}>
|
||||
{note.summary}
|
||||
{article.summary}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoteTopZaps
|
||||
topZaps={note.topZaps}
|
||||
topZaps={article.topZaps}
|
||||
zapCount={reactionsState.zapCount}
|
||||
users={store.users}
|
||||
action={() => {}}
|
||||
/>
|
||||
|
||||
<PrimalMarkdown
|
||||
noteId={props.naddr}
|
||||
content={note.content || ''}
|
||||
content={article.content || ''}
|
||||
readonly={true} />
|
||||
|
||||
<div class={styles.tags}>
|
||||
<For each={note.tags}>
|
||||
<For each={article.tags}>
|
||||
{tag => (
|
||||
<div class={styles.tag}>
|
||||
{tag}
|
||||
@ -533,6 +701,11 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
</div> */}
|
||||
</Show>
|
||||
</div>
|
||||
<div>
|
||||
<For each={store.replies}>
|
||||
{reply => <Note note={reply} />}
|
||||
</For>
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user