Compare commits

...

2 Commits

Author SHA1 Message Date
Bojan Mojsilovic
1105d39ca7 Show actual sats zapped for articles 2024-06-17 16:59:52 +02:00
Bojan Mojsilovic
03dd582b68 Fix mention rendering in long notes 2024-06-17 16:46:39 +02:00
8 changed files with 216 additions and 106 deletions

View File

@ -31,15 +31,15 @@ const ArticlePreview: Component<{
const thread = useThreadContext();
const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
likes: props.article.likes,
liked: props.article.noteActions.liked,
reposts: props.article.reposts,
reposted: props.article.noteActions.reposted,
replies: props.article.replies,
replied: props.article.noteActions.replied,
zapCount: props.article.zaps,
satsZapped: props.article.satszapped,
zapped: props.article.noteActions.zapped,
likes: props.article.likes || 0,
liked: props.article.noteActions?.liked || false,
reposts: props.article.reposts || 0,
reposted: props.article.noteActions?.reposted || false,
replies: props.article.replies || 0,
replied: props.article.noteActions?.replied || false,
zapCount: props.article.zaps || 0,
satsZapped: props.article.satszapped || 0,
zapped: props.article.noteActions?.zapped || false,
zappedAmount: 0,
zappedNow: false,
isZapping: false,
@ -218,7 +218,7 @@ const ArticlePreview: Component<{
<Avatar user={props.article.user} size="micro"/>
<div class={styles.userName}>{userName(props.article.user)}</div>
<VerificationCheck user={props.article.user} />
<div class={styles.nip05}>{props.article.user.nip05 || ''}</div>
<div class={styles.nip05}>{props.article.user?.nip05 || ''}</div>
</div>
<div class={styles.time}>
{shortDate(props.article.published)}
@ -239,14 +239,14 @@ const ArticlePreview: Component<{
<div class={styles.estimate}>
{Math.ceil(props.article.wordCount / 238)} minute read
</div>
<For each={props.article.tags.slice(0, 3)}>
<For each={props.article.tags?.slice(0, 3)}>
{tag => (
<A href={`/reads/${tag}`} class={styles.tag}>
{tag}
</A>
)}
</For>
<Show when={props.article.tags.length > 3}>
<Show when={props.article.tags?.length && props.article.tags.length > 3}>
<div class={styles.tag}>
+ {props.article.tags.length - 3}
</div>
@ -263,7 +263,7 @@ const ArticlePreview: Component<{
</div>
</div>
<Show when={props.article.topZaps.length > 0}>
<Show when={props.article.topZaps?.length > 0}>
<div class={styles.zaps}>
<NoteTopZapsCompact
note={props.article}

View File

@ -10,7 +10,7 @@
grid-template-rows: 32px 1fr;
grid-row-gap: 8px;
text-decoration: none;
color: unset;
color: var(--text-primary);
line-height: 20px;
&:hover {

View File

@ -16,11 +16,12 @@ import VerificationCheck from '../VerificationCheck/VerificationCheck';
import styles from './EmbeddedNote.module.scss';
const EmbeddedNote: Component<{
note: PrimalNote,
note: PrimalNote | undefined,
mentionedUsers?: Record<string, PrimalUser>,
includeEmbeds?: boolean,
isLast?: boolean,
alternativeBackground?: boolean,
class?: string,
}> = (props) => {
const threadContext = useThreadContext();
@ -28,21 +29,22 @@ const EmbeddedNote: Component<{
let noteContent: HTMLDivElement | undefined;
const noteId = () => nip19.noteEncode(props.note.post.id);
const noteId = () => nip19.noteEncode(props.note?.post.id);
const navToThread = () => {
threadContext?.actions.setPrimaryNote(props.note);
};
const verification = createMemo(() => {
return trimVerification(props.note.user?.nip05);
return trimVerification(props.note?.user?.nip05);
});
const klass = () => {
let k = styles.mentionedNote;
k += ' embeddedNote';
if (props.isLast) k+= ' noBottomMargin';
if (props.alternativeBackground) k+= ` ${styles.altBack}`;
if (props.isLast) k += ' noBottomMargin';
if (props.alternativeBackground) k += ` ${styles.altBack}`;
if (props.class) k += ` ${props.class}`;
return k;
}
@ -52,7 +54,7 @@ const EmbeddedNote: Component<{
return (
<div
class={klass()}
data-event={props.note.post.id}
data-event={props.note?.post.id}
data-event-bech32={noteId()}
>
{children}
@ -65,7 +67,7 @@ const EmbeddedNote: Component<{
href={`/e/${noteId()}`}
class={klass()}
onClick={() => navToThread()}
data-event={props.note.post.id}
data-event={props.note?.post.id}
data-event-bech32={noteId()}
>
{children}
@ -77,37 +79,37 @@ const EmbeddedNote: Component<{
<>
<div class={styles.mentionedNoteHeader}>
<Avatar
user={props.note.user}
user={props.note?.user}
size="xxs"
/>
<span class={styles.postInfo}>
<span class={styles.userInfo}>
<Show
when={props.note.user.nip05}
when={props.note?.user.nip05}
fallback={
<span class={styles.userName}>
{userName(props.note.user)}
{userName(props.note?.user)}
</span>
}
>
<span class={styles.userName}>
{verification()[0]}
</span>
<VerificationCheck user={props.note.user} />
<VerificationCheck user={props.note?.user} />
<span
class={styles.verifiedBy}
title={props.note.user.nip05}
title={props.note?.user.nip05}
>
{nip05Verification(props.note.user)}
{nip05Verification(props.note?.user)}
</span>
</Show>
</span>
<span
class={styles.time}
title={date(props.note.post.created_at || 0).date.toLocaleString()}
title={date(props.note?.post.created_at || 0).date.toLocaleString()}
>
{date(props.note.post.created_at || 0).label}
{date(props.note?.post.created_at || 0).label}
</span>
</span>
</div>

View File

@ -25,10 +25,12 @@ import { getParametrizedEvent, getParametrizedEvents } from '../../lib/notes';
import { decodeIdentifier } from '../../lib/keys';
import ArticleShort from '../ArticlePreview/ArticleShort';
import { userName } from '../../stores/profile';
import { useIntl } from '@cookbook/solid-intl';
const ArticleSidebar: Component< { id?: string, user: PrimalUser, article: PrimalArticle } > = (props) => {
const intl = useIntl();
const account = useAccountContext();
const [recomended, setRecomended] = createStore<PrimalArticle[]>([]);
@ -55,17 +57,19 @@ const ArticleSidebar: Component< { id?: string, user: PrimalUser, article: Prima
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>
<Show when={props.article.satszapped > 0}>
<div class={styles.headingPicks}>
Total zaps
</div>
</div>
<div class={styles.section}>
<div class={styles.totalZaps}>
<span class={styles.totalZapsIcon} />
<span class={styles.amount}>{intl.formatNumber(props.article.satszapped)}</span>
<span class={styles.unit}>sats</span>
</div>
</div>
</Show>
<Show
when={!isFetchingArticles()}

View File

@ -0,0 +1,92 @@
import { A } from '@solidjs/router';
import { Component, createSignal, Show } from 'solid-js';
import { useMediaContext } from '../../contexts/MediaContext';
import { hookForDev } from '../../lib/devTools';
import styles from './LinkPreview.module.scss';
const errorCountLimit = 3;
const ArticleLinkPreview: Component<{ preview: any, id?: string, bordered?: boolean, isLast?: boolean }> = (props) => {
const media = useMediaContext();
const image = () => {
const i = media?.actions.getMedia(props.preview.images[0] || '', 'm');
return i;
};
const ratio = () => {
const img = image();
if (!img) return 2;
return img.w / img.h;
};
const height = () => {
const img = image();
if (!img || ratio() <= 1.2) return 'auto';
// width of the note over the ratio of the preview image
const h = 524 / ratio();
return `${h}px`;
};
const klass = () => {
let k = image() && ratio() <= 1.2 ? styles.linkPreviewH : styles.linkPreview;
if (props.bordered) {
k += ` ${styles.bordered}`;
}
k += " embeddedContent";
k += props.isLast ? ' noBottomMargin' : '';
return k;
};
const [errorCount, setErrorCount] = createSignal(0);
const onError = (event: any) => {
if (errorCount() > errorCountLimit) return;
setErrorCount(v => v + 1);
const image = event.target;
image.onerror = '';
image.src = props.preview.images[0];
return true;
};
return (
<A
id={props.id}
href={props.preview.url}
class={klass()}
>
<Show when={errorCount() < errorCountLimit && (image() || props.preview.images[0])}>
<img
class={styles.previewImage}
src={image()?.media_url || props.preview.images[0]}
style={`width: 100%; height: ${height()}`}
onerror={onError}
/>
</Show>
<div class={styles.previewInfo}>
<Show when={props.preview.title}>
<div class={styles.previewTitle}>{props.preview.title}</div>
</Show>
<Show when={props.preview.description}>
<div class={styles.previewDescription}>{props.preview.description}</div>
</Show>
</div>
</A>
);
}
export default hookForDev(ArticleLinkPreview);

View File

@ -18,6 +18,13 @@
margin-bottom: 20px;
}
span {
color: var(--text-primary);
font-size: 16px;
font-weight: 400;
line-height: 24px;
}
h1 {
color: var(--text-primary);
font-size: 32px;
@ -174,8 +181,12 @@
border-left: 4px solid var(--text-tertiary);
}
a {
color: var(--accent-links);
&:not(.embeddedNote) {
color: var(--accent-links);
}
font-size: 16px;
font-weight: 400;
line-height: 24px;

View File

@ -21,14 +21,14 @@ import styles from './PrimalMarkdown.module.scss';
import ButtonPrimary from '../Buttons/ButtonPrimary';
import ButtonGhost from '../Buttons/ButtonGhost';
import { Ctx } from '@milkdown/ctx';
import { npubToHex } from '../../lib/keys';
import { decodeIdentifier, npubToHex } from '../../lib/keys';
import { subscribeTo } from '../../sockets';
import { APP_ID } from '../../App';
import { getUserProfileInfo } from '../../lib/profile';
import { useAccountContext } from '../../contexts/AccountContext';
import { Kind } from '../../constants';
import { PrimalArticle, PrimalNote, PrimalUser } from '../../types/primal';
import { convertToUser, userName } from '../../stores/profile';
import { authorName, convertToUser, userName } from '../../stores/profile';
import { A } from '@solidjs/router';
import { createStore } from 'solid-js/store';
import { nip19 } from 'nostr-tools';
@ -37,21 +37,23 @@ import { logError } from '../../lib/logger';
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
import NoteImage from '../NoteImage/NoteImage';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import { template } from 'solid-js/web';
import ArticlePreview from '../ArticlePreview/ArticlePreview';
import LinkPreview from '../LinkPreview/LinkPreview';
import ArticleLinkPreview from '../LinkPreview/ArticleLinkPreview';
const PrimalMarkdown: Component<{
id?: string,
content?: string,
readonly?: boolean,
noteId: string,
article: PrimalArticle | undefined,
}> = (props) => {
const account = useAccountContext();
let ref: HTMLDivElement | undefined;
let editor: Editor;
const [userMentions, setUserMentions] = createStore<Record<string, PrimalUser>>({});
const [noteMentions, setNoteMentions] = createStore<Record<string, PrimalNote>>({});
const id = () => {
return `note_${props.noteId}`;
}
@ -70,47 +72,6 @@ const account = useAccountContext();
lightbox.init();
});
const fetchUserInfo = (npub: string) => {
const pubkey = npubToHex(npub);
const subId = `lf_fui_${APP_ID}`;
let user: PrimalUser;
const unsub = subscribeTo(subId, (type, _, content) => {
if (type === 'EOSE') {
unsub();
setUserMentions(() => ({ [user.npub]: { ...user }}))
return;
}
if (type === 'EVENT') {
if (content?.kind === Kind.Metadata) {
user = convertToUser(content);
}
}
});
getUserProfileInfo(pubkey, account?.publicKey, subId);
}
const fetchNoteInfo = async (npub: string) => {
const noteId = nip19.decode(npub).data;
const subId = `lf_fni_${APP_ID}`;
try {
const notes = await fetchNotes(account?.publicKey, [noteId], subId);
if (notes.length > 0) {
const note = notes[0];
setNoteMentions(() => ({ [note.post.noteId]: { ...note } }))
}
} catch (e) {
logError('Failed to fetch notes: ', e);
}
}
const isMention = (el: Element) => {
const regex = /nostr:([A-z0-9]+)/;
const content = el.innerHTML;
@ -141,34 +102,63 @@ const account = useAccountContext();
if (match === null || match.length < 2) return el;
const [nostr, id] = match;
const [nostr, npub] = match;
if (id.startsWith('npub')) {
if (npub.startsWith('npub')) {
fetchUserInfo(id);
const other = content.split(nostr);
const id = npubToHex(npub);
return (
<p>
<span innerHTML={other[0] || ''}></span>
<Show
when={!props.article?.mentionedUsers || props.article.mentionedUsers[id] !== undefined}
fallback={<A href={`/p/${npub}`}>{nostr}</A>}
>
<A href={`/p/${npub}`}>@{userName(props.article?.mentionedUsers ? props.article.mentionedUsers[id] : undefined)}</A>
</Show>
<span innerHTML={other[1] || ''}></span>
</p>
);
}
if (npub.startsWith('note')) {
const id = npubToHex(npub);
return (
<Show
when={userMentions[id] !== undefined}
fallback={<A href={`/p/${id}`}>{nostr}</A>}
when={!props.article?.mentionedNotes || props.article.mentionedNotes[id] !== undefined}
fallback={<A href={`/e/${npub}`}>{nostr}</A>}
>
<A href={`/p/${id}`}>@{userName(userMentions[id])}</A>
<div class={styles.embeddedNote}>
<EmbeddedNote
class={styles.embeddedNote}
note={props.article?.mentionedNotes && props.article.mentionedNotes[id]}
mentionedUsers={props.article?.mentionedNotes && props.article.mentionedNotes[id].mentionedUsers || {}}
/>
</div>
</Show>
);
}
if (id.startsWith('note')) {
fetchNoteInfo(id);
return (
<Show
when={noteMentions[id] !== undefined}
fallback={<A href={`/e/${id}`}>{nostr}</A>}
>
<EmbeddedNote
note={noteMentions[id]}
mentionedUsers={noteMentions[id].mentionedUsers || {}}
/>
</Show>
);
if (npub.startsWith('naddr')) {
const mention = props.article?.mentionedNotes && props.article.mentionedNotes[npub];
if (!mention) return el;
const preview = {
url: `/e/${npub}`,
description: (mention.post.tags.find(t => t[0] === 'summary') || [])[1] || mention.post.content.slice(0, 100),
images: [(mention.post.tags.find(t => t[0] === 'image') || [])[1] || mention.user.picture],
title: (mention.post.tags.find(t => t[0] === 'title') || [])[1] || authorName(mention.user),
}
console.log('MENTION: ', mention)
return <ArticleLinkPreview
preview={preview}
bordered={true}
/>;
}
return el;

View File

@ -713,8 +713,18 @@ const Longform: Component< { naddr: string } > = (props) => {
const mentionContent = content as NostrMentionContent;
const mention = JSON.parse(mentionContent.content);
let id = mention.id;
if (mention.kind === Kind.LongForm) {
id = nip19.naddrEncode({
identifier: (mention.tags.find((t: string[]) => t[0] === 'd') || [])[1],
pubkey: mention.pubkey,
kind: mention.kind,
});
}
updateStore('page', 'mentions',
(mentions) => ({ ...mentions, [mention.id]: { ...mention } })
(mentions) => ({ ...mentions, [id]: { ...mention } })
);
return;
}
@ -977,6 +987,7 @@ const Longform: Component< { naddr: string } > = (props) => {
noteId={props.naddr}
content={store.article?.content || ''}
readonly={true}
article={store.article}
/>
<div class={styles.tags}>