mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Compare commits
48 Commits
9e10e7a9f3
...
fb2e26cb23
Author | SHA1 | Date | |
---|---|---|---|
|
fb2e26cb23 | ||
|
a69f7d5950 | ||
|
4440e0d499 | ||
|
23d8bb980b | ||
|
f309b2bfc0 | ||
|
40bc2ec8a7 | ||
|
4524f7d4fc | ||
|
de05c5d0a1 | ||
|
bd09ed7668 | ||
|
fcb3926e67 | ||
|
388c2e689d | ||
|
829675481d | ||
|
ebdda42433 | ||
|
5b5afea786 | ||
|
e445b11019 | ||
|
cfa16f5964 | ||
|
1ce4ecd7da | ||
|
b7b99350a8 | ||
|
635b504d5e | ||
|
f22a02318c | ||
|
0081870198 | ||
|
622dad8ecc | ||
|
26d8885b9c | ||
|
d32d2af392 | ||
|
70a935bc6f | ||
|
254499a4e4 | ||
|
b4b51a242d | ||
|
b1ad4299eb | ||
|
219f3ab084 | ||
|
b606e90532 | ||
|
e28299e9f0 | ||
|
a7eec46a27 | ||
|
2fc28409cd | ||
|
f7e59e4f9b | ||
|
d152ac47d7 | ||
|
77494df791 | ||
|
f9b790ac58 | ||
|
e1d565e939 | ||
|
2993a97e7f | ||
|
3914a3c0c9 | ||
|
cca4d11df0 | ||
|
e075c7741f | ||
|
129aca7a54 | ||
|
8f1a53d2ed | ||
|
786fda989a | ||
|
52b4aba426 | ||
|
6a2ce12501 | ||
|
4c7de1e307 |
3
src/assets/icons/dot.svg
Normal file
3
src/assets/icons/dot.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="6" height="6" viewBox="0 0 6 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.2 5.384C2.76267 5.384 2.35733 5.28267 1.984 5.08C1.62133 4.86667 1.33333 4.57867 1.12 4.216C0.906667 3.85333 0.8 3.45333 0.8 3.016C0.8 2.568 0.906667 2.168 1.12 1.816C1.33333 1.45333 1.62133 1.17067 1.984 0.967999C2.35733 0.754666 2.76267 0.648 3.2 0.648C3.648 0.648 4.05333 0.754666 4.416 0.967999C4.77867 1.17067 5.06667 1.45333 5.28 1.816C5.504 2.168 5.616 2.568 5.616 3.016C5.616 3.45333 5.504 3.85333 5.28 4.216C5.06667 4.57867 4.77867 4.86667 4.416 5.08C4.05333 5.28267 3.648 5.384 3.2 5.384Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 626 B |
BIN
src/assets/images/reads_image_dark.png
Normal file
BIN
src/assets/images/reads_image_dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/images/reads_image_light.png
Normal file
BIN
src/assets/images/reads_image_light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
@ -1,4 +1,5 @@
|
||||
.article {
|
||||
.article, .articleShort {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
@ -43,6 +44,10 @@
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 14px;
|
||||
|
||||
&::before {
|
||||
content: '• ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,17 +66,28 @@
|
||||
.content {
|
||||
.title {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
line-height: 32px;
|
||||
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.summary {
|
||||
color: var(--text-primary);
|
||||
font-family: Lora;
|
||||
color: var(--brand-text);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 5;
|
||||
line-clamp: 5;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,24 +104,45 @@
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
width: fit-content;
|
||||
margin: 4px;
|
||||
margin-block: 4px;
|
||||
margin-inline: 3px;
|
||||
}
|
||||
.estimate {
|
||||
display: inline-block;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
font-weight: 600;
|
||||
line-height: 12px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--subtile-devider );
|
||||
border-radius: 12px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
min-width: 164px;
|
||||
min-height: 70px;
|
||||
max-height: 240px;
|
||||
height: fit-content;
|
||||
border: 1px solid var(--devider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
width: 164px;
|
||||
max-width: 162px;
|
||||
max-height: 238px;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.placeholderImage {
|
||||
width: 164px;
|
||||
height: 118px;
|
||||
background-image: var(--reads-placeholder-image);
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -115,3 +152,65 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.upRightFloater {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.articleShort {
|
||||
padding: 0;
|
||||
.header {
|
||||
.userInfo {
|
||||
.userName {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
.body {
|
||||
.text {
|
||||
.content {
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.estimate {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.image {
|
||||
min-width: 100px;
|
||||
height: fit-content;
|
||||
border: 1px solid var(--devider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
img {
|
||||
width: 100px;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.placeholderImage {
|
||||
width: 100px;
|
||||
height: 70px;
|
||||
background-image: var(--reads-placeholder-image);
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import { PrimalArticle, ZapOption } from '../../types/primal';
|
||||
import { uuidv4 } from '../../utils';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import { NoteReactionsState } from '../Note/Note';
|
||||
import NoteContextTrigger from '../Note/NoteContextTrigger';
|
||||
import ArticleFooter from '../Note/NoteFooter/ArticleFooter';
|
||||
import NoteFooter from '../Note/NoteFooter/NoteFooter';
|
||||
import NoteTopZaps from '../Note/NoteTopZaps';
|
||||
@ -53,6 +54,7 @@ const ArticlePreview: Component<{
|
||||
|
||||
let latestTopZap: string = '';
|
||||
let latestTopZapFeed: string = '';
|
||||
let articleContextMenu: HTMLDivElement | undefined;
|
||||
|
||||
const onConfirmZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
@ -181,19 +183,48 @@ const ArticlePreview: Component<{
|
||||
onCancel: onCancelZap,
|
||||
});
|
||||
|
||||
const openReactionModal = (openOn = 'likes') => {
|
||||
app?.actions.openReactionModal(props.article.id, {
|
||||
likes: reactionsState.likes,
|
||||
zaps: reactionsState.zapCount,
|
||||
reposts: reactionsState.reposts,
|
||||
quotes: reactionsState.quoteCount,
|
||||
openOn,
|
||||
});
|
||||
};
|
||||
|
||||
const onContextMenuTrigger = () => {
|
||||
app?.actions.openContextMenu(
|
||||
props.article,
|
||||
articleContextMenu?.getBoundingClientRect(),
|
||||
() => {
|
||||
app?.actions.openCustomZapModal(customZapInfo());
|
||||
},
|
||||
openReactionModal,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<A class={styles.article} href={`/e/${props.article.naddr}`}>
|
||||
<div class={styles.upRightFloater}>
|
||||
<NoteContextTrigger
|
||||
ref={articleContextMenu}
|
||||
onClick={onContextMenuTrigger}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles.header}>
|
||||
<div class={styles.userInfo}>
|
||||
<Avatar user={props.article.author} size="micro"/>
|
||||
<div class={styles.userName}>{userName(props.article.author)}</div>
|
||||
<VerificationCheck user={props.article.author} />
|
||||
<div class={styles.nip05}>{props.article.author.nip05 || ''}</div>
|
||||
<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>
|
||||
<div class={styles.time}>
|
||||
{shortDate(props.article.published)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.body}>
|
||||
<div class={styles.text}>
|
||||
<div class={styles.content}>
|
||||
@ -205,20 +236,30 @@ const ArticlePreview: Component<{
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.tags}>
|
||||
<For each={props.article.tags}>
|
||||
<div class={styles.estimate}>
|
||||
{Math.ceil(props.article.wordCount / 238)} minute read
|
||||
</div>
|
||||
<For each={props.article.tags.slice(0, 3)}>
|
||||
{tag => (
|
||||
<div class={styles.tag}>
|
||||
{tag}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class={styles.estimate}>
|
||||
{Math.ceil(props.article.wordCount / 238)} minute read
|
||||
</div>
|
||||
<Show when={props.article.tags.length > 3}>
|
||||
<div class={styles.tag}>
|
||||
+ {props.article.tags.length - 3}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.image}>
|
||||
<img src={props.article.image} />
|
||||
<Show
|
||||
when={props.article.image}
|
||||
fallback={<div class={styles.placeholderImage}></div>}
|
||||
>
|
||||
<img src={props.article.image} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
65
src/components/ArticlePreview/ArticleShort.tsx
Normal file
65
src/components/ArticlePreview/ArticleShort.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { A } from '@solidjs/router';
|
||||
import { batch, Component, createEffect, For, JSXElement, Show } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { Portal } from 'solid-js/web';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { CustomZapInfo, useAppContext } from '../../contexts/AppContext';
|
||||
import { useThreadContext } from '../../contexts/ThreadContext';
|
||||
import { date, shortDate } from '../../lib/dates';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { userName } from '../../stores/profile';
|
||||
import { PrimalArticle, ZapOption } from '../../types/primal';
|
||||
import { uuidv4 } from '../../utils';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import { NoteReactionsState } from '../Note/Note';
|
||||
import NoteContextTrigger from '../Note/NoteContextTrigger';
|
||||
import ArticleFooter from '../Note/NoteFooter/ArticleFooter';
|
||||
import NoteFooter from '../Note/NoteFooter/NoteFooter';
|
||||
import NoteTopZaps from '../Note/NoteTopZaps';
|
||||
import NoteTopZapsCompact from '../Note/NoteTopZapsCompact';
|
||||
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
||||
|
||||
import styles from './ArticlePreview.module.scss';
|
||||
|
||||
const ArticlePreview: Component<{
|
||||
id?: string,
|
||||
article: PrimalArticle,
|
||||
}> = (props) => {
|
||||
|
||||
return (
|
||||
<A class={styles.articleShort} href={`/e/${props.article.noteId}`}>
|
||||
<div class={styles.header}>
|
||||
<div class={styles.userInfo}>
|
||||
<Avatar user={props.article.user} size="micro"/>
|
||||
<div class={styles.userName}>{userName(props.article.user)}</div>
|
||||
</div>
|
||||
<div class={styles.time}>
|
||||
{date(props.article.published).label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.body}>
|
||||
<div class={styles.text}>
|
||||
<div class={styles.content}>
|
||||
<div class={styles.title}>
|
||||
{props.article.title}
|
||||
</div>
|
||||
<div class={styles.estimate}>
|
||||
{Math.ceil(props.article.wordCount / 238)} minutes
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.image}>
|
||||
<Show
|
||||
when={props.article.image}
|
||||
fallback={<div class={styles.placeholderImage}></div>}
|
||||
>
|
||||
<img src={props.article.image} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</A>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ArticlePreview);
|
66
src/components/AuthorSubscribe/AuthorSubscribe.module.scss
Normal file
66
src/components/AuthorSubscribe/AuthorSubscribe.module.scss
Normal file
@ -0,0 +1,66 @@
|
||||
.authorSubscribeCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--background-header-input);
|
||||
width: 300px;
|
||||
padding: 16px;
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.userData {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.userName {
|
||||
display: flex;
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nip05 {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userPitch {
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
>button {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
padding-inline: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
153
src/components/AuthorSubscribe/AuthorSubscribe.tsx
Normal file
153
src/components/AuthorSubscribe/AuthorSubscribe.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { A, useNavigate } from '@solidjs/router';
|
||||
import { batch, Component, createEffect, createSignal, For, JSXElement, onMount, Show } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { Portal } from 'solid-js/web';
|
||||
import { APP_ID } from '../../App';
|
||||
import { Kind } from '../../constants';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { CustomZapInfo, useAppContext } from '../../contexts/AppContext';
|
||||
import { useThreadContext } from '../../contexts/ThreadContext';
|
||||
import { fetchUserProfile } from '../../handleNotes';
|
||||
import { date, shortDate } from '../../lib/dates';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { sendEvent } from '../../lib/notes';
|
||||
import { zapSubscription } from '../../lib/zap';
|
||||
import { userName } from '../../stores/profile';
|
||||
import { PrimalArticle, PrimalUser, ZapOption } from '../../types/primal';
|
||||
import { uuidv4 } from '../../utils';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import ButtonSecondary from '../Buttons/ButtonSecondary';
|
||||
import Loader from '../Loader/Loader';
|
||||
import { Tier, TierCost } from '../SubscribeToAuthorModal/SubscribeToAuthorModal';
|
||||
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
||||
|
||||
import styles from './AuthorSubscribe.module.scss';
|
||||
|
||||
const AuthoreSubscribe: Component<{
|
||||
id?: string,
|
||||
pubkey: string,
|
||||
}> = (props) => {
|
||||
const account = useAccountContext();
|
||||
const app = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isFetching, setIsFetching] = createSignal(false);
|
||||
const [author, setAuthor] = createSignal<PrimalUser>();
|
||||
|
||||
const getAuthorData = async () => {
|
||||
if (!account?.publicKey) return;
|
||||
|
||||
const subId = `reads_fpi_${APP_ID}`;
|
||||
|
||||
setIsFetching(() => true);
|
||||
|
||||
const profile = await fetchUserProfile(account.publicKey, props.pubkey, subId);
|
||||
|
||||
setIsFetching(() => false);
|
||||
|
||||
setAuthor(() => ({ ...profile }));
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
getAuthorData();
|
||||
});
|
||||
|
||||
const doSubscription = async (tier: Tier, cost: TierCost, exchangeRate?: Record<string, Record<string, number>>) => {
|
||||
const a = author();
|
||||
|
||||
if (!a || !account || !cost) return;
|
||||
|
||||
if (cost.unit === 'USD' && (!exchangeRate || !exchangeRate['USD'])) return;
|
||||
|
||||
const subEvent = {
|
||||
kind: Kind.Subscribe,
|
||||
content: '',
|
||||
created_at: Math.floor((new Date()).getTime() / 1_000),
|
||||
tags: [
|
||||
['p', a.pubkey],
|
||||
['e', tier.id],
|
||||
['amount', cost.amount, cost.unit, cost.cadence],
|
||||
['event', JSON.stringify(tier.event)],
|
||||
// Copy any zap splits
|
||||
...(tier.event.tags?.filter(t => t[0] === 'zap') || []),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
const { success, note } = await sendEvent(subEvent, account.relays, account.relaySettings);
|
||||
|
||||
if (success && note) {
|
||||
const isZapped = await zapSubscription(note, a, account.publicKey, account.relays, exchangeRate);
|
||||
|
||||
if (!isZapped) {
|
||||
unsubscribe(note.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = async (eventId: string) => {
|
||||
const a = author();
|
||||
|
||||
if (!a || !account) return;
|
||||
|
||||
const unsubEvent = {
|
||||
kind: Kind.Unsubscribe,
|
||||
content: '',
|
||||
created_at: Math.floor((new Date()).getTime() / 1_000),
|
||||
|
||||
tags: [
|
||||
['p', a.pubkey],
|
||||
['e', eventId],
|
||||
],
|
||||
};
|
||||
|
||||
await sendEvent(unsubEvent, account.relays, account.relaySettings);
|
||||
|
||||
}
|
||||
|
||||
const openSubscribe = () => {
|
||||
app?.actions.openAuthorSubscribeModal(author(), doSubscription);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={styles.featuredAuthor}>
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={<Loader />}
|
||||
>
|
||||
<div class={styles.authorSubscribeCard}>
|
||||
<div class={styles.userInfo}>
|
||||
<Avatar user={author()} size="ml" />
|
||||
<div class={styles.userData}>
|
||||
<div class={styles.userName}>
|
||||
{userName(author())}
|
||||
<VerificationCheck user={author()} />
|
||||
</div>
|
||||
<div class={styles.nip05}>
|
||||
{author()?.nip05}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.userPitch}>
|
||||
{author()?.about || ''}
|
||||
</div>
|
||||
<div class={styles.actions}>
|
||||
<ButtonSecondary
|
||||
light={true}
|
||||
onClick={() => navigate(`/p/${author()?.npub}`)}
|
||||
>
|
||||
view profile
|
||||
</ButtonSecondary>
|
||||
|
||||
<ButtonPrimary onClick={openSubscribe}>
|
||||
subscribe
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(AuthoreSubscribe);
|
@ -164,6 +164,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mlAvatar {
|
||||
@include avatar;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
|
||||
.missingBack {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.iconBackground {
|
||||
@include iconBackground;
|
||||
}
|
||||
}
|
||||
|
||||
.largeAvatar {
|
||||
@include avatar;
|
||||
width: 72px;
|
||||
@ -290,6 +305,13 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mlMissing {
|
||||
@include missing;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.largeMissing {
|
||||
@include missing;
|
||||
width: 68px;
|
||||
|
@ -11,7 +11,7 @@ import styles from './Avatar.module.scss';
|
||||
|
||||
const Avatar: Component<{
|
||||
src?: string | undefined,
|
||||
size?: "micro" | "xxs" | "xss" | "xs" | "vvs" | "vs" | "sm" | "md" | "lg" | "xl" | "xxl",
|
||||
size?: "micro" | "xxs" | "xss" | "xs" | "vvs" | "vs" | "sm" | "md" | "ml" | "lg" | "xl" | "xxl",
|
||||
user?: PrimalUser,
|
||||
highlightBorder?: boolean,
|
||||
id?: string,
|
||||
@ -34,6 +34,7 @@ const Avatar: Component<{
|
||||
vs: styles.vsAvatar,
|
||||
sm: styles.smallAvatar,
|
||||
md: styles.midAvatar,
|
||||
ml: styles.mlAvatar,
|
||||
lg: styles.largeAvatar,
|
||||
xl: styles.extraLargeAvatar,
|
||||
xxl: styles.xxlAvatar,
|
||||
@ -48,6 +49,7 @@ const Avatar: Component<{
|
||||
vs: styles.vsMissing,
|
||||
sm: styles.smallMissing,
|
||||
md: styles.midMissing,
|
||||
ml: styles.mlMissing,
|
||||
lg: styles.largeMissing,
|
||||
xl: styles.extraLargeMissing,
|
||||
xxl: styles.xxlMissing,
|
||||
|
@ -16,7 +16,7 @@ import styles from './BookmarkNote.module.scss';
|
||||
import { saveBookmarks } from '../../lib/localStore';
|
||||
import { importEvents, triggerImportEvents } from '../../lib/notes';
|
||||
|
||||
const BookmarkArticle: Component<{ note: PrimalArticle, large?: boolean }> = (props) => {
|
||||
const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: boolean }> = (props) => {
|
||||
const account = useAccountContext();
|
||||
const app = useAppContext();
|
||||
const intl = useIntl();
|
||||
@ -24,8 +24,13 @@ const BookmarkArticle: Component<{ note: PrimalArticle, large?: boolean }> = (pr
|
||||
const [isBookmarked, setIsBookmarked] = createSignal(false);
|
||||
const [bookmarkInProgress, setBookmarkInProgress] = createSignal(false);
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
setIsBookmarked(() => account?.bookmarks.includes(props.note.id) || false);
|
||||
const note = props.note;
|
||||
|
||||
if (note) {
|
||||
setIsBookmarked(() => account?.bookmarks.includes(note.id) || false);
|
||||
}
|
||||
})
|
||||
|
||||
const updateBookmarks = async (bookmarkTags: string[][]) => {
|
||||
@ -47,7 +52,7 @@ const BookmarkArticle: Component<{ note: PrimalArticle, large?: boolean }> = (pr
|
||||
};
|
||||
|
||||
const addBookmark = async (bookmarkTags: string[][]) => {
|
||||
if (account && !bookmarkTags.find(b => b[0] === 'e' && b[1] === props.note.id)) {
|
||||
if (account && props.note && !bookmarkTags.find(b => b[0] === 'e' && b[1] === props.note?.id)) {
|
||||
const bookmarksToAdd = [...bookmarkTags, ['e', props.note.id]];
|
||||
|
||||
if (bookmarksToAdd.length < 2) {
|
||||
@ -73,8 +78,8 @@ const BookmarkArticle: Component<{ note: PrimalArticle, large?: boolean }> = (pr
|
||||
}
|
||||
|
||||
const removeBookmark = async (bookmarks: string[][]) => {
|
||||
if (account && bookmarks.find(b => b[0] === 'e' && b[1] === props.note.id)) {
|
||||
const bookmarksToAdd = bookmarks.filter(b => b[0] !== 'e' || b[1] !== props.note.id);
|
||||
if (account && bookmarks.find(b => b[0] === 'e' && b[1] === props.note?.id)) {
|
||||
const bookmarksToAdd = bookmarks.filter(b => b[0] !== 'e' || b[1] !== props.note?.id);
|
||||
|
||||
if (bookmarksToAdd.length < 1) {
|
||||
logWarning('BOOKMARK ISSUE: ', `before_bookmark_${APP_ID}`);
|
||||
|
@ -37,3 +37,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-left: 6px !important;
|
||||
padding-right: 0px !important;
|
||||
|
||||
&.rightL {
|
||||
width: 22px !important;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import styles from './BookmarkNote.module.scss';
|
||||
import { saveBookmarks } from '../../lib/localStore';
|
||||
import { importEvents, triggerImportEvents } from '../../lib/notes';
|
||||
|
||||
const BookmarkNote: Component<{ note: PrimalNote, large?: boolean }> = (props) => {
|
||||
const BookmarkNote: Component<{ note: PrimalNote, large?: boolean, right?: boolean }> = (props) => {
|
||||
const account = useAccountContext();
|
||||
const app = useAppContext();
|
||||
const intl = useIntl();
|
||||
@ -136,6 +136,7 @@ const BookmarkNote: Component<{ note: PrimalNote, large?: boolean }> = (props) =
|
||||
return (
|
||||
<div class={styles.bookmark}>
|
||||
<ButtonGhost
|
||||
class={`${props.right ? styles.right : ''} ${props.large ? styles.rightL : ''}`}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -118,6 +118,7 @@ const EmbeddedNote: Component<{
|
||||
shorten={true}
|
||||
isEmbeded={true}
|
||||
width={noteContent?.getBoundingClientRect().width}
|
||||
margins={2}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -8,7 +8,7 @@ import { FeedOption, PrimalFeed, SelectionOption } from '../../types/primal';
|
||||
import SelectBox from '../SelectBox/SelectBox';
|
||||
import SelectionBox from '../SelectionBox/SelectionBox';
|
||||
|
||||
const FeedSelect: Component<{ isPhone?: boolean, id?: string}> = (props) => {
|
||||
const FeedSelect: Component<{ isPhone?: boolean, id?: string, big?: boolean}> = (props) => {
|
||||
|
||||
const account = useAccountContext();
|
||||
const home = useHomeContext();
|
||||
@ -123,6 +123,7 @@ const FeedSelect: Component<{ isPhone?: boolean, id?: string}> = (props) => {
|
||||
value={selectedValue()}
|
||||
isSelected={isSelected}
|
||||
isPhone={props.isPhone}
|
||||
big={props.big}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
129
src/components/FeedSelect/ReedSelect.tsx
Normal file
129
src/components/FeedSelect/ReedSelect.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { Component } from 'solid-js';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { useHomeContext } from '../../contexts/HomeContext';
|
||||
import { useReadsContext } from '../../contexts/ReadsContext';
|
||||
import { useSettingsContext } from '../../contexts/SettingsContext';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { fetchStoredFeed } from '../../lib/localStore';
|
||||
import { FeedOption, PrimalFeed, SelectionOption } from '../../types/primal';
|
||||
import SelectBox from '../SelectBox/SelectBox';
|
||||
import SelectionBox from '../SelectionBox/SelectionBox';
|
||||
|
||||
const ReedSelect: Component<{ isPhone?: boolean, id?: string, big?: boolean}> = (props) => {
|
||||
|
||||
const account = useAccountContext();
|
||||
const reeds = useReadsContext();
|
||||
const settings = useSettingsContext();
|
||||
|
||||
const findFeed = (hex: string, includeReplies: string) => {
|
||||
const ir = includeReplies === 'undefined' ? undefined :
|
||||
includeReplies === 'true';
|
||||
|
||||
return settings?.availableFeeds.find(f => {
|
||||
const isHex = f.hex === hex;
|
||||
const isOpt = typeof ir === typeof f.includeReplies ?
|
||||
f.includeReplies === ir :
|
||||
false;
|
||||
|
||||
return isHex && isOpt;
|
||||
});
|
||||
};
|
||||
|
||||
const selectFeed = (option: FeedOption) => {
|
||||
|
||||
const [hex, includeReplies] = option.value?.split('_') || [];
|
||||
// const selector = document.getElementById('defocus');
|
||||
|
||||
// selector?.focus();
|
||||
// selector?.blur();
|
||||
|
||||
const feed = {
|
||||
hex: option.value,
|
||||
name: option.label,
|
||||
};
|
||||
|
||||
reeds?.actions.clearNotes();
|
||||
reeds?.actions.selectFeed(feed);
|
||||
};
|
||||
|
||||
const isSelected = (option: FeedOption) => {
|
||||
const selected = reeds?.selectedFeed;
|
||||
|
||||
|
||||
if (selected?.hex && option.value) {
|
||||
const t = option.value.split('_');
|
||||
|
||||
const isHex = encodeURI(selected.hex) == t[0];
|
||||
const isOpt = t[1] === 'undefined' ?
|
||||
selected.includeReplies === undefined :
|
||||
selected.includeReplies?.toString() === t[1];
|
||||
|
||||
return isHex && isOpt;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const options:() => SelectionOption[] = () => {
|
||||
let opts = [];
|
||||
|
||||
if (account?.publicKey) {
|
||||
opts.push(
|
||||
{
|
||||
label: 'My Reads',
|
||||
value: account?.publicKey || '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
opts.push(
|
||||
{
|
||||
label: 'All Reads',
|
||||
value: 'none',
|
||||
}
|
||||
);
|
||||
|
||||
return [ ...opts ];
|
||||
};
|
||||
|
||||
const initialValue = () => {
|
||||
const selected = reeds?.selectedFeed;
|
||||
|
||||
if (!selected) {
|
||||
const feed = options()[0];
|
||||
selectFeed(feed);
|
||||
return feed;
|
||||
}
|
||||
|
||||
return {
|
||||
label: selected.name,
|
||||
value: selected.hex || '',
|
||||
}
|
||||
}
|
||||
|
||||
const selectedValue = () => {
|
||||
if (!reeds?.selectedFeed)
|
||||
return initialValue();
|
||||
|
||||
const value = `${encodeURI(reeds.selectedFeed.hex || '')}`;
|
||||
|
||||
return {
|
||||
label: reeds.selectedFeed.name,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectionBox
|
||||
options={options()}
|
||||
onChange={selectFeed}
|
||||
initialValue={initialValue()}
|
||||
value={selectedValue()}
|
||||
isSelected={isSelected}
|
||||
isPhone={props.isPhone}
|
||||
big={props.big}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ReedSelect);
|
@ -123,6 +123,13 @@
|
||||
border-bottom: 1px solid var(--devider);
|
||||
z-index: var(--z-index-header);
|
||||
|
||||
&.readsFeed {
|
||||
position: relative;
|
||||
border-bottom: none;
|
||||
padding-inline: 0;
|
||||
z-index: var(--z-index-header);
|
||||
}
|
||||
|
||||
|
||||
.newContentItem {
|
||||
color: var(--accent-links);
|
||||
@ -155,7 +162,7 @@
|
||||
outline: none;
|
||||
|
||||
padding-block: 21px;
|
||||
padding-inline: 12px;
|
||||
padding-inline: 20px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
justify-content: flex-start;
|
||||
|
@ -119,7 +119,7 @@ const HomeHeader: Component< {
|
||||
<Show
|
||||
when={settings?.availableFeeds && settings?.availableFeeds.length > 0 && home?.selectedFeed}
|
||||
>
|
||||
<FeedSelect />
|
||||
<FeedSelect big={true} />
|
||||
</Show>
|
||||
|
||||
<Show
|
||||
|
37
src/components/HomeHeader/ReadsHeader.tsx
Normal file
37
src/components/HomeHeader/ReadsHeader.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Component, createSignal, For, onCleanup, onMount, Show } from 'solid-js';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
|
||||
import styles from './HomeHeader.module.scss';
|
||||
import FeedSelect from '../FeedSelect/FeedSelect';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import SmallCallToAction from '../SmallCallToAction/SmallCallToAction';
|
||||
import { useHomeContext } from '../../contexts/HomeContext';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { useSettingsContext } from '../../contexts/SettingsContext';
|
||||
import { placeholders as t, actions as tActions, feedNewPosts } from '../../translations';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import CreateAccountModal from '../CreateAccountModal/CreateAccountModal';
|
||||
import LoginModal from '../LoginModal/LoginModal';
|
||||
import { userName } from '../../stores/profile';
|
||||
import { PrimalUser } from '../../types/primal';
|
||||
import ReedSelect from '../FeedSelect/ReedSelect';
|
||||
|
||||
const ReadsHeader: Component< {
|
||||
id?: string,
|
||||
hasNewPosts: () => boolean,
|
||||
loadNewContent: () => void,
|
||||
newPostCount: () => number,
|
||||
newPostAuthors: PrimalUser[],
|
||||
} > = (props) => {
|
||||
|
||||
return (
|
||||
<div id={props.id}>
|
||||
<div class={`${styles.bigFeedSelect} ${styles.readsFeed}`}>
|
||||
<ReedSelect big={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ReadsHeader);
|
96
src/components/HomeSidebar/ArticleSidebar.tsx
Normal file
96
src/components/HomeSidebar/ArticleSidebar.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
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 { fetchUserArticles } 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 fetchUserArticles(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>
|
||||
|
||||
<Show
|
||||
when={!isFetchingArticles()}
|
||||
fallback={
|
||||
<Loader />
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={recomended.length > 0}
|
||||
>
|
||||
<div class={styles.headingReads}>
|
||||
More Reads from {userName(props.article.user)}
|
||||
</div>
|
||||
|
||||
<div class={styles.section}>
|
||||
<For each={recomended}>
|
||||
{(note) => <ArticleShort article={note} />}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ArticleSidebar);
|
@ -56,3 +56,99 @@
|
||||
margin-top: 34px;
|
||||
z-index: 10px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-left: -8px;
|
||||
|
||||
.section {
|
||||
margin-bottom: 28px;
|
||||
|
||||
>a:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
240
src/components/HomeSidebar/ReadsSidebar.tsx
Normal file
240
src/components/HomeSidebar/ReadsSidebar.tsx
Normal file
@ -0,0 +1,240 @@
|
||||
import { Component, createEffect, createSignal, For, onMount, Show } from 'solid-js';
|
||||
|
||||
import {
|
||||
EventCoordinate,
|
||||
PrimalArticle,
|
||||
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, getFeaturedAuthors, getReadsTopics } from '../../lib/feed';
|
||||
import { fetchArticles } from '../../handleNotes';
|
||||
import { getParametrizedEvent, getParametrizedEvents } from '../../lib/notes';
|
||||
import { decodeIdentifier } from '../../lib/keys';
|
||||
import ArticleShort from '../ArticlePreview/ArticleShort';
|
||||
import AuthorSubscribe from '../AuthorSubscribe/AuthorSubscribe';
|
||||
|
||||
const sidebarOptions = [
|
||||
{
|
||||
label: 'Trending 24h',
|
||||
value: 'trending_24h',
|
||||
},
|
||||
{
|
||||
label: 'Trending 12h',
|
||||
value: 'trending_12h',
|
||||
},
|
||||
{
|
||||
label: 'Trending 4h',
|
||||
value: 'trending_4h',
|
||||
},
|
||||
{
|
||||
label: 'Trending 1h',
|
||||
value: 'trending_1h',
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
value: '',
|
||||
disabled: true,
|
||||
separator: true,
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Most-zapped 24h',
|
||||
value: 'mostzapped_24h',
|
||||
},
|
||||
{
|
||||
label: 'Most-zapped 12h',
|
||||
value: 'mostzapped_12h',
|
||||
},
|
||||
{
|
||||
label: 'Most-zapped 4h',
|
||||
value: 'mostzapped_4h',
|
||||
},
|
||||
{
|
||||
label: 'Most-zapped 1h',
|
||||
value: 'mostzapped_1h',
|
||||
},
|
||||
];
|
||||
|
||||
const ReadsSidebar: Component< { id?: string } > = (props) => {
|
||||
|
||||
const account = useAccountContext();
|
||||
const reads= useReadsContext();
|
||||
|
||||
const [topPicks, setTopPicks] = createStore<PrimalArticle[]>([]);
|
||||
const [topics, setTopics] = createStore<string[]>([]);
|
||||
const [featuredAuthor, setFeautredAuthor] = createSignal<string>();
|
||||
|
||||
const [isFetching, setIsFetching] = createSignal(false);
|
||||
const [isFetchingTopics, setIsFetchingTopics] = createSignal(false);
|
||||
const [isFetchingAuthors, setIsFetchingAuthors] = createSignal(false);
|
||||
|
||||
const [got, setGot] = createSignal(false);
|
||||
|
||||
const getTopics = () => {
|
||||
const subId = `reads_topics_${APP_ID}`;
|
||||
|
||||
const unsub = subsTo(subId, {
|
||||
onEvent: (_, content) => {
|
||||
const topics = JSON.parse(content.content || '[]') as string[];
|
||||
|
||||
setTopics(() => [...topics]);
|
||||
},
|
||||
onEose: () => {
|
||||
setIsFetchingTopics(() => false);
|
||||
unsub();
|
||||
}
|
||||
})
|
||||
setIsFetchingTopics(() => true);
|
||||
getReadsTopics(subId);
|
||||
}
|
||||
|
||||
const getFeaturedAuthor = () => {
|
||||
const subId = `reads_fa_${APP_ID}`;
|
||||
|
||||
const unsub = subsTo(subId, {
|
||||
onEvent: (_, content) => {
|
||||
const authors = JSON.parse(content.content || '[]') as string[];
|
||||
|
||||
// const author = '1d22e00c32fcf2eb60c094f89f5cfa3ccd38a1b317dccda9b296fa6f50e00d0e';
|
||||
// setFeautredAuthor(() => author);
|
||||
|
||||
// const author = 'a8eb6e07bf408713b0979f337a3cd978f622e0d41709f3b74b48fff43dbfcd2b';
|
||||
// setFeautredAuthor(() => author);
|
||||
|
||||
// const author = '88cc134b1a65f54ef48acc1df3665063d3ea45f04eab8af4646e561c5ae99079';
|
||||
// setFeautredAuthor(() => author);
|
||||
|
||||
// const author = 'fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52';
|
||||
// setFeautredAuthor(() => author);
|
||||
|
||||
// const author = '3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24';
|
||||
// setFeautredAuthor(() => author);
|
||||
|
||||
setFeautredAuthor(() => authors[Math.floor(Math.random() * authors.length)]);
|
||||
},
|
||||
onEose: () => {
|
||||
setIsFetchingAuthors(() => false);
|
||||
unsub();
|
||||
}
|
||||
})
|
||||
setIsFetchingAuthors(() => true);
|
||||
getFeaturedAuthors(subId);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (account?.isKeyLookupDone && reads?.recomendedReads.length === 0) {
|
||||
reads.actions.doSidebarSearch('');
|
||||
}
|
||||
|
||||
if (account?.isKeyLookupDone) {
|
||||
getTopics();
|
||||
getFeaturedAuthor();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
const rec = reads?.recomendedReads || [];
|
||||
|
||||
if (rec.length > 0 && !got()) {
|
||||
setGot(() => true);
|
||||
let randomIndices = new Set<number>();
|
||||
|
||||
while (randomIndices.size < 3) {
|
||||
const randomIndex = Math.floor(Math.random() * rec.length);
|
||||
randomIndices.add(randomIndex);
|
||||
}
|
||||
|
||||
const reads = [ ...randomIndices ].map(i => rec[i]);
|
||||
|
||||
getRecomendedArticles(reads)
|
||||
}
|
||||
});
|
||||
|
||||
const getRecomendedArticles = async (ids: string[]) => {
|
||||
// if (!account?.publicKey) return;
|
||||
|
||||
const subId = `reads_picks_${APP_ID}`;
|
||||
|
||||
setIsFetching(() => true);
|
||||
|
||||
const articles = await fetchArticles(ids,subId);
|
||||
|
||||
setIsFetching(() => false);
|
||||
|
||||
setTopPicks(() => [...articles]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div id={props.id} class={styles.readsSidebar}>
|
||||
<Show when={account?.isKeyLookupDone}>
|
||||
<Show when={account?.publicKey}>
|
||||
<div class={styles.headingPicks}>
|
||||
Featured Author
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={!isFetchingAuthors()}
|
||||
fallback={
|
||||
<Loader />
|
||||
}
|
||||
>
|
||||
<div class={styles.section}>
|
||||
<AuthorSubscribe pubkey={featuredAuthor()} />
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<div class={styles.headingPicks}>
|
||||
Featured Reads
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={
|
||||
<Loader />
|
||||
}
|
||||
>
|
||||
<div class={styles.section}>
|
||||
<For each={topPicks}>
|
||||
{(note) => <ArticleShort article={note} />}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
||||
<div class={styles.headingPicks}>
|
||||
Topics
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={!isFetchingTopics()}
|
||||
fallback={
|
||||
<Loader />
|
||||
}
|
||||
>
|
||||
<div class={styles.section}>
|
||||
<For each={topics}>
|
||||
{(topic) => <div class={styles.topic}>{topic}</div>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(ReadsSidebar);
|
@ -21,6 +21,7 @@ import NoteContextMenu from '../Note/NoteContextMenu';
|
||||
import LnQrCodeModal from '../LnQrCodeModal/LnQrCodeModal';
|
||||
import ConfirmModal from '../ConfirmModal/ConfirmModal';
|
||||
import CashuQrCodeModal from '../CashuQrCodeModal/CashuQrCodeModal';
|
||||
import SubscribeToAuthorModal from '../SubscribeToAuthorModal/SubscribeToAuthorModal';
|
||||
|
||||
export const [isHome, setIsHome] = createSignal(false);
|
||||
|
||||
@ -191,6 +192,12 @@ const Layout: Component = () => {
|
||||
onConfirm={app?.confirmInfo?.onConfirm}
|
||||
onAbort={app?.confirmInfo?.onAbort}
|
||||
/>
|
||||
|
||||
<SubscribeToAuthorModal
|
||||
author={app?.subscribeToAuthor}
|
||||
onClose={app?.actions.closeAuthorSubscribeModal}
|
||||
onSubscribe={app?.subscribeToTier}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -115,10 +115,11 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 18px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
|
||||
margin-left: 2px;
|
||||
margin-top: -8px;
|
||||
|
||||
font-family: 'Roboto Condensed';
|
||||
font-weight: 500;
|
||||
|
@ -13,7 +13,7 @@ import { getUserProfiles } from "../../../lib/profile";
|
||||
import { subscribeTo } from "../../../sockets";
|
||||
import { convertToNotes, referencesToTags } from "../../../stores/note";
|
||||
import { convertToUser, nip05Verification, truncateNpub, userName } from "../../../stores/profile";
|
||||
import { EmojiOption, FeedPage, NostrMentionContent, NostrNoteContent, NostrStatsContent, NostrUserContent, PrimalNote, PrimalUser, SendNoteResult } from "../../../types/primal";
|
||||
import { EmojiOption, FeedPage, NostrMentionContent, NostrNoteContent, NostrStatsContent, NostrUserContent, PrimalArticle, PrimalNote, PrimalUser, SendNoteResult } from "../../../types/primal";
|
||||
import { debounce, getScreenCordinates, isVisibleInContainer, uuidv4 } from "../../../utils";
|
||||
import Avatar from "../../Avatar/Avatar";
|
||||
import EmbeddedNote from "../../EmbeddedNote/EmbeddedNote";
|
||||
@ -52,7 +52,7 @@ type AutoSizedTextArea = HTMLTextAreaElement & { _baseScrollHeight: number };
|
||||
|
||||
const EditBox: Component<{
|
||||
id?: string,
|
||||
replyToNote?: PrimalNote,
|
||||
replyToNote?: PrimalNote | PrimalArticle,
|
||||
onClose?: () => void,
|
||||
onSuccess?: (note: SendNoteResult) => void,
|
||||
open?: boolean,
|
||||
@ -587,8 +587,8 @@ const EditBox: Component<{
|
||||
|
||||
createEffect(() => {
|
||||
if (props.open) {
|
||||
const draft = readNoteDraft(account?.publicKey, props.replyToNote?.post.noteId);
|
||||
const draftUserRefs = readNoteDraftUserRefs(account?.publicKey, props.replyToNote?.post.noteId);
|
||||
const draft = readNoteDraft(account?.publicKey, props.replyToNote?.noteId);
|
||||
const draftUserRefs = readNoteDraftUserRefs(account?.publicKey, props.replyToNote?.noteId);
|
||||
|
||||
setUserRefs(reconcile(draftUserRefs));
|
||||
|
||||
@ -618,8 +618,8 @@ const EditBox: Component<{
|
||||
if (message().length === 0) return;
|
||||
|
||||
// save draft just in case there is an unintended interuption
|
||||
saveNoteDraft(account?.publicKey, message(), props.replyToNote?.post.noteId);
|
||||
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.post.noteId);
|
||||
saveNoteDraft(account?.publicKey, message(), props.replyToNote?.noteId);
|
||||
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.noteId);
|
||||
});
|
||||
|
||||
const onEscape = (e: KeyboardEvent) => {
|
||||
@ -667,8 +667,8 @@ const EditBox: Component<{
|
||||
return;
|
||||
}
|
||||
|
||||
saveNoteDraft(account?.publicKey, '', props.replyToNote?.post.noteId);
|
||||
saveNoteDraftUserRefs(account?.publicKey, {}, props.replyToNote?.post.noteId);
|
||||
saveNoteDraft(account?.publicKey, '', props.replyToNote?.noteId);
|
||||
saveNoteDraftUserRefs(account?.publicKey, {}, props.replyToNote?.noteId);
|
||||
clearEditor();
|
||||
};
|
||||
|
||||
@ -680,8 +680,8 @@ const EditBox: Component<{
|
||||
};
|
||||
|
||||
const persistNote = (note: string) => {
|
||||
saveNoteDraft(account?.publicKey, note, props.replyToNote?.post.noteId);
|
||||
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.post.noteId);
|
||||
saveNoteDraft(account?.publicKey, note, props.replyToNote?.noteId);
|
||||
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.noteId);
|
||||
clearEditor();
|
||||
};
|
||||
|
||||
@ -721,10 +721,10 @@ const EditBox: Component<{
|
||||
const rep = props.replyToNote;
|
||||
|
||||
if (rep) {
|
||||
let rootTag = rep.post.tags.find(t => t[0] === 'e' && t[3] === 'root');
|
||||
let rootTag = rep.msg.tags.find(t => t[0] === 'e' && t[3] === 'root');
|
||||
|
||||
const rHints = (rep.post.relayHints && rep.post.relayHints[rep.post.id]) ?
|
||||
rep.post.relayHints[rep.post.id] :
|
||||
const rHints = (rep.relayHints && rep.relayHints[rep.id]) ?
|
||||
rep.relayHints[rep.id] :
|
||||
'';
|
||||
|
||||
// If the note has a root tag, that meens it is not a root note itself
|
||||
@ -735,26 +735,26 @@ const EditBox: Component<{
|
||||
v,
|
||||
);
|
||||
tags.push([...tagWithHint]);
|
||||
tags.push(['e', rep.post.id, rHints, 'reply']);
|
||||
tags.push(['e', rep.id, rHints, 'reply']);
|
||||
}
|
||||
// Otherwise, add the note as the root tag for this reply
|
||||
else {
|
||||
tags.push([
|
||||
'e',
|
||||
rep.post.id,
|
||||
rep.id,
|
||||
rHints,
|
||||
'root',
|
||||
]);
|
||||
}
|
||||
|
||||
// Copy all `p` tags from the note we are repling to
|
||||
const repPeople = rep.post.tags.filter(t => t[0] === 'p');
|
||||
const repPeople = rep.msg.tags.filter(t => t[0] === 'p');
|
||||
|
||||
tags = [...tags, ...(unwrap(repPeople))];
|
||||
|
||||
// If the author of the note is missing, add them
|
||||
if (!tags.find(t => t[0] === 'p' && t[1] === rep.post.pubkey)) {
|
||||
tags.push(['p', rep.post.pubkey]);
|
||||
if (!tags.find(t => t[0] === 'p' && t[1] === rep.pubkey)) {
|
||||
tags.push(['p', rep.pubkey]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -772,7 +772,7 @@ const EditBox: Component<{
|
||||
toast?.sendSuccess(intl.formatMessage(tToast.publishNoteSuccess));
|
||||
props.onSuccess && props.onSuccess({ success, reasons, note });
|
||||
setIsPostingInProgress(false);
|
||||
saveNoteDraft(account.publicKey, '', rep?.post.noteId)
|
||||
saveNoteDraft(account.publicKey, '', rep?.noteId)
|
||||
clearEditor();
|
||||
}
|
||||
unsub();
|
||||
@ -1046,7 +1046,7 @@ const EditBox: Component<{
|
||||
|
||||
// // setNoteRefs((refs) => ({
|
||||
// // ...refs,
|
||||
// // [newNote.post.noteId]: newNote
|
||||
// // [newNote.noteId]: newNote
|
||||
// // }));
|
||||
|
||||
subUserRef(hex);
|
||||
@ -1107,10 +1107,10 @@ const EditBox: Component<{
|
||||
|
||||
setNoteRefs((refs) => ({
|
||||
...refs,
|
||||
[newNote.post.noteId]: newNote
|
||||
[newNote.noteId]: newNote
|
||||
}));
|
||||
|
||||
subNoteRef(newNote.post.noteId);
|
||||
subNoteRef(newNote.noteId);
|
||||
|
||||
unsub();
|
||||
return;
|
||||
|
@ -3,7 +3,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
padding: 12px 20px;
|
||||
margin: 0px;
|
||||
|
||||
text-decoration: none;
|
||||
@ -361,12 +361,16 @@
|
||||
background-color: var(--background-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-inline: 12px;
|
||||
padding-inline: 0px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 12px;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
|
||||
.header {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-area: content;
|
||||
display: flex;
|
||||
@ -386,6 +390,7 @@
|
||||
line-height: 24px;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
padding-inline: 12px;
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
@ -406,6 +411,7 @@
|
||||
|
||||
.time {
|
||||
padding-block: 20px;
|
||||
padding-inline: 12px;
|
||||
border-bottom: 1px solid var(--devider);
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-tertiary);
|
||||
@ -445,6 +451,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.topZaps {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,7 +276,10 @@ const Note: Component<{
|
||||
<div class={styles.noteNotifications}>
|
||||
<div class={styles.content}>
|
||||
<div class={styles.message}>
|
||||
<ParsedNote note={props.note} shorten={true} />
|
||||
<ParsedNote
|
||||
note={props.note}
|
||||
shorten={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles.footer}>
|
||||
@ -285,6 +288,7 @@ const Note: Component<{
|
||||
state={reactionsState}
|
||||
updateState={updateReactionsState}
|
||||
customZapInfo={customZapInfo()}
|
||||
size="short"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -301,7 +305,9 @@ const Note: Component<{
|
||||
>
|
||||
<div class={styles.border}></div>
|
||||
|
||||
<NoteHeader note={props.note} primary={true} />
|
||||
<div class={styles.header}>
|
||||
<NoteHeader note={props.note} primary={true} />
|
||||
</div>
|
||||
|
||||
<div class={styles.upRightFloater}>
|
||||
<NoteContextTrigger
|
||||
@ -313,14 +319,20 @@ const Note: Component<{
|
||||
<div class={styles.content}>
|
||||
|
||||
<div class={styles.message}>
|
||||
<ParsedNote note={props.note} width={Math.min(640, window.innerWidth)} />
|
||||
<ParsedNote
|
||||
note={props.note}
|
||||
width={Math.min(640, window.innerWidth)}
|
||||
margins={12}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NoteTopZaps
|
||||
topZaps={reactionsState.topZaps}
|
||||
zapCount={reactionsState.zapCount}
|
||||
action={() => openReactionModal('zaps')}
|
||||
/>
|
||||
<div class={styles.topZaps}>
|
||||
<NoteTopZaps
|
||||
topZaps={reactionsState.topZaps}
|
||||
zapCount={reactionsState.zapCount}
|
||||
action={() => openReactionModal('zaps')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={styles.time}
|
||||
@ -337,15 +349,17 @@ const Note: Component<{
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={reactionsState}
|
||||
updateState={updateReactionsState}
|
||||
customZapInfo={customZapInfo()}
|
||||
wide={true}
|
||||
large={true}
|
||||
onZapAnim={addTopZap}
|
||||
/>
|
||||
<div class={styles.footer}>
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={reactionsState}
|
||||
updateState={updateReactionsState}
|
||||
customZapInfo={customZapInfo()}
|
||||
size="wide"
|
||||
large={true}
|
||||
onZapAnim={addTopZap}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
@ -390,6 +404,7 @@ const Note: Component<{
|
||||
note={props.note}
|
||||
shorten={props.shorten}
|
||||
width={Math.min(640, window.innerWidth - 72)}
|
||||
margins={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -406,7 +421,6 @@ const Note: Component<{
|
||||
updateState={updateReactionsState}
|
||||
customZapInfo={customZapInfo()}
|
||||
onZapAnim={addTopZapFeed}
|
||||
wide={true}
|
||||
/>
|
||||
</A>
|
||||
</Match>
|
||||
@ -457,7 +471,8 @@ const Note: Component<{
|
||||
<ParsedNote
|
||||
note={props.note}
|
||||
shorten={props.shorten}
|
||||
width={Math.min(528, window.innerWidth - 72)}
|
||||
width={Math.min(566, window.innerWidth - 72)}
|
||||
margins={1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -468,13 +483,16 @@ const Note: Component<{
|
||||
topZapLimit={4}
|
||||
/>
|
||||
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={reactionsState}
|
||||
updateState={updateReactionsState}
|
||||
customZapInfo={customZapInfo()}
|
||||
onZapAnim={addTopZapFeed}
|
||||
/>
|
||||
<div class={styles.footer}>
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={reactionsState}
|
||||
updateState={updateReactionsState}
|
||||
customZapInfo={customZapInfo()}
|
||||
onZapAnim={addTopZapFeed}
|
||||
size="short"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</A>
|
||||
@ -515,6 +533,7 @@ const Note: Component<{
|
||||
note={props.note}
|
||||
shorten={props.shorten}
|
||||
width={Math.min(528, window.innerWidth - 72)}
|
||||
margins={12}
|
||||
noLightbox={true}
|
||||
altEmbeds={true}
|
||||
/>
|
||||
|
@ -60,12 +60,12 @@ const NoteContextMenu: Component<{
|
||||
|
||||
|
||||
const doMuteUser = () => {
|
||||
account?.actions.addToMuteList(note()?.post.pubkey);
|
||||
account?.actions.addToMuteList(note()?.pubkey);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const doUnmuteUser = () => {
|
||||
account?.actions.removeFromMuteList(note()?.post.pubkey);
|
||||
account?.actions.removeFromMuteList(note()?.pubkey);
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
@ -77,21 +77,21 @@ const NoteContextMenu: Component<{
|
||||
|
||||
const copyNoteLink = () => {
|
||||
if (!props.data) return;
|
||||
navigator.clipboard.writeText(`${window.location.origin}/e/${note().post.noteId}`);
|
||||
navigator.clipboard.writeText(`${window.location.origin}/e/${note().noteId}`);
|
||||
props.onClose()
|
||||
toaster?.sendSuccess(intl.formatMessage(tToast.notePrimalLinkCoppied));
|
||||
};
|
||||
|
||||
const copyNoteText = () => {
|
||||
if (!props.data) return;
|
||||
navigator.clipboard.writeText(`${note().post.content}`);
|
||||
navigator.clipboard.writeText(`${note().content}`);
|
||||
props.onClose()
|
||||
toaster?.sendSuccess(intl.formatMessage(tToast.notePrimalTextCoppied));
|
||||
};
|
||||
|
||||
const copyNoteId = () => {
|
||||
if (!props.data) return;
|
||||
navigator.clipboard.writeText(`${note().post.noteId}`);
|
||||
navigator.clipboard.writeText(`${note().noteId}`);
|
||||
props.onClose()
|
||||
toaster?.sendSuccess(intl.formatMessage(tToast.noteIdCoppied));
|
||||
};
|
||||
@ -128,7 +128,7 @@ const NoteContextMenu: Component<{
|
||||
const onClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
!props.data ||
|
||||
!document?.getElementById(`note_context_${note().post.id}`)?.contains(e.target as Node)
|
||||
!document?.getElementById(`note_context_${note().id}`)?.contains(e.target as Node)
|
||||
) {
|
||||
props.onClose()
|
||||
}
|
||||
@ -222,7 +222,7 @@ const NoteContextMenu: Component<{
|
||||
];
|
||||
};
|
||||
|
||||
const noteContext = () => account?.publicKey !== note()?.post.pubkey ?
|
||||
const noteContext = () => account?.publicKey !== note()?.pubkey ?
|
||||
[ ...noteContextForEveryone, ...noteContextForOtherPeople()] :
|
||||
noteContextForEveryone;
|
||||
|
||||
@ -251,7 +251,7 @@ const NoteContextMenu: Component<{
|
||||
/>
|
||||
|
||||
<PrimalMenu
|
||||
id={`note_context_${note()?.post.id}`}
|
||||
id={`note_context_${note()?.id}`}
|
||||
items={noteContext()}
|
||||
hidden={!props.open}
|
||||
position="note_footer"
|
||||
|
@ -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(),
|
||||
@ -181,7 +183,7 @@ const ArticleFooter: Component<{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canUserReceiveZaps(props.note.author)) {
|
||||
if (!canUserReceiveZaps(props.note.user)) {
|
||||
toast?.sendWarning(
|
||||
intl.formatMessage(t.zapUnavailable),
|
||||
);
|
||||
@ -206,7 +208,7 @@ const ArticleFooter: Component<{
|
||||
return;
|
||||
}
|
||||
|
||||
if (account.relays.length === 0 || !canUserReceiveZaps(props.note.author)) {
|
||||
if (account.relays.length === 0 || !canUserReceiveZaps(props.note.user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
@ -55,12 +55,38 @@
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
grid-template-columns: 125px 125px 125px 125px auto;
|
||||
grid-template-columns: 145px 145px 145px 145px auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.bookmarkFoot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
max-width: 20px;
|
||||
}
|
||||
|
||||
&.wide {
|
||||
grid-template-columns: 137px 137px 137px 135px auto;
|
||||
grid-template-columns: 148px 148px 148px 148px auto;
|
||||
|
||||
.bookmarkFoot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.short {
|
||||
grid-template-columns: 126px 126px 126px 126px auto;
|
||||
|
||||
.bookmarkFoot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.normal {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.context {
|
||||
@ -172,10 +198,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bookmarkFoot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.largeZapLottie {
|
||||
|
@ -27,7 +27,7 @@ export const lottieDuration = () => zapMD.op * 1_000 / zapMD.fr;
|
||||
|
||||
const NoteFooter: Component<{
|
||||
note: PrimalNote,
|
||||
wide?: boolean,
|
||||
size?: 'wide' | 'normal' | 'short',
|
||||
id?: string,
|
||||
state: NoteReactionsState,
|
||||
updateState: SetStoreFunction<NoteReactionsState>,
|
||||
@ -48,6 +48,8 @@ const NoteFooter: Component<{
|
||||
let footerDiv: HTMLDivElement | undefined;
|
||||
let repostMenu: HTMLDivElement | undefined;
|
||||
|
||||
const size = () => props.size ?? 'normal';
|
||||
|
||||
const repostMenuItems: MenuItem[] = [
|
||||
{
|
||||
action: () => doRepost(),
|
||||
@ -61,6 +63,7 @@ const NoteFooter: Component<{
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const onClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
!document?.getElementById(`repost_menu_${props.note.post.id}`)?.contains(e.target as Node)
|
||||
@ -222,12 +225,17 @@ const NoteFooter: 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`;
|
||||
@ -318,7 +326,12 @@ const NoteFooter: 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
|
||||
@ -395,6 +408,7 @@ const NoteFooter: Component<{
|
||||
<BookmarkNote
|
||||
note={props.note}
|
||||
large={props.large}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -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 (
|
||||
|
||||
|
@ -147,6 +147,7 @@ const ParsedNote: Component<{
|
||||
shorten?: boolean,
|
||||
isEmbeded?: boolean,
|
||||
width?: number,
|
||||
margins?: number,
|
||||
noLightbox?: boolean,
|
||||
altEmbeds?: boolean,
|
||||
}> = (props) => {
|
||||
@ -619,9 +620,10 @@ const ParsedNote: Component<{
|
||||
let w: number | undefined = undefined;
|
||||
|
||||
if (mVideo) {
|
||||
const margins = props.margins || 20
|
||||
const ratio = mVideo.w / mVideo.h;
|
||||
h = (noteWidth() / ratio);
|
||||
w = h > 680 ? 680 * ratio : noteWidth();
|
||||
h = ((noteWidth() - 2*margins) / ratio);
|
||||
w = h > 680 ? 680 * ratio : noteWidth() - 2*margins;
|
||||
h = h > 680 ? 680 : h;
|
||||
}
|
||||
|
||||
|
@ -9,33 +9,229 @@
|
||||
.editor {
|
||||
margin-block: 8px;
|
||||
|
||||
* {
|
||||
p {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
p, li {
|
||||
font-family: Lora;
|
||||
font-size: 15px;
|
||||
font-style: normal;
|
||||
h1 {
|
||||
color: var(--text-primary);
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
line-height: 40px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--text-primary);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
line-height: 36px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--text-primary);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: var(--text-primary);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 30px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 26px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top:1px solid var(--subtile-devider);
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 0px;
|
||||
padding-left: 14px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
li {
|
||||
&::marker {
|
||||
content: '•';
|
||||
}
|
||||
padding-left: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
ul {
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-left: 0px;
|
||||
padding-left: 24px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
li {
|
||||
padding-left: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
ol {
|
||||
padding-left: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dl {
|
||||
margin-left: 0px;
|
||||
padding-left: 14px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
dt {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
dd {
|
||||
padding-left: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
dl {
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img + sup {
|
||||
display: block;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
|
||||
blockquote {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
line-height: 24px;
|
||||
padding-left: 12px;
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid var(--text-tertiary);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-links);
|
||||
font-family: Lora;
|
||||
font-size: 15px;
|
||||
font-style: normal;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
pre, code, mark {
|
||||
background-color: var(--background-input);
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--text-primary);
|
||||
font-family: "Fira Mono", monospace;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
ins {
|
||||
color: var(--warning-bright);
|
||||
}
|
||||
|
||||
del {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
table {
|
||||
th {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
border-bottom: 1px solid var(--text-secondary);
|
||||
padding: 12px 8px;
|
||||
* {
|
||||
margin-bottom: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
border-bottom: 1px solid var(--subtile-devider);
|
||||
padding: 12px 8px;
|
||||
* {
|
||||
margin-bottom: 0;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import { APP_ID } from '../../App';
|
||||
import { getUserProfileInfo } from '../../lib/profile';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { Kind } from '../../constants';
|
||||
import { PrimalNote, PrimalUser } from '../../types/primal';
|
||||
import { PrimalArticle, PrimalNote, PrimalUser } from '../../types/primal';
|
||||
import { convertToUser, userName } from '../../stores/profile';
|
||||
import { A } from '@solidjs/router';
|
||||
import { createStore } from 'solid-js/store';
|
||||
@ -35,11 +35,14 @@ import { nip19 } from 'nostr-tools';
|
||||
import { fetchNotes } from '../../handleNotes';
|
||||
import { logError } from '../../lib/logger';
|
||||
import EmbeddedNote from '../EmbeddedNote/EmbeddedNote';
|
||||
import NoteImage from '../NoteImage/NoteImage';
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
|
||||
const PrimalMarkdown: Component<{
|
||||
id?: string,
|
||||
content?: string,
|
||||
readonly?: boolean,
|
||||
noteId: string,
|
||||
}> = (props) => {
|
||||
const account = useAccountContext();
|
||||
|
||||
@ -49,6 +52,24 @@ const account = useAccountContext();
|
||||
const [userMentions, setUserMentions] = createStore<Record<string, PrimalUser>>({});
|
||||
const [noteMentions, setNoteMentions] = createStore<Record<string, PrimalNote>>({});
|
||||
|
||||
const id = () => {
|
||||
return `note_${props.noteId}`;
|
||||
}
|
||||
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
gallery: `#${id()}`,
|
||||
children: `a.image_${props.noteId}`,
|
||||
showHideAnimationType: 'zoom',
|
||||
initialZoomLevel: 'fit',
|
||||
secondaryZoomLevel: 2,
|
||||
maxZoomLevel: 3,
|
||||
pswpModule: () => import('photoswipe')
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
lightbox.init();
|
||||
});
|
||||
|
||||
const fetchUserInfo = (npub: string) => {
|
||||
const pubkey = npubToHex(npub);
|
||||
|
||||
@ -97,6 +118,20 @@ const account = useAccountContext();
|
||||
return regex.test(content)
|
||||
}
|
||||
|
||||
const isImg = (el: Element) => {
|
||||
// @ts-ignore
|
||||
return el.firstChild?.tagName === 'IMG';
|
||||
}
|
||||
|
||||
const renderImage = (el: Element) => {
|
||||
const img = el.firstChild as HTMLImageElement;
|
||||
|
||||
return <NoteImage
|
||||
class={`noteimage image_${props.noteId}`}
|
||||
src={img.src}
|
||||
/>
|
||||
}
|
||||
|
||||
const renderMention = (el: Element) => {
|
||||
const regex = /nostr:([A-z0-9]+)/;
|
||||
|
||||
@ -108,7 +143,7 @@ const account = useAccountContext();
|
||||
|
||||
const [nostr, id] = match;
|
||||
|
||||
if (id.startsWith('npub1')) {
|
||||
if (id.startsWith('npub')) {
|
||||
|
||||
fetchUserInfo(id);
|
||||
return (
|
||||
@ -121,7 +156,7 @@ const account = useAccountContext();
|
||||
);
|
||||
}
|
||||
|
||||
if (id.startsWith('note1')) {
|
||||
if (id.startsWith('note')) {
|
||||
fetchNoteInfo(id);
|
||||
return (
|
||||
<Show
|
||||
@ -199,13 +234,16 @@ const account = useAccountContext();
|
||||
|
||||
<div ref={ref} class={styles.editor} style="display: none;" />
|
||||
|
||||
<div class={styles.editor}>
|
||||
<div id={id()} class={styles.editor}>
|
||||
<For each={htmlArray()}>
|
||||
{el => (
|
||||
<Switch fallback={<>{el}</>}>
|
||||
<Match when={isMention(el)}>
|
||||
{renderMention(el)}
|
||||
</Match>
|
||||
<Match when={isImg(el)}>
|
||||
{renderImage(el)}
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
|
@ -13,6 +13,7 @@ import { store } from "../../services/StoreService";
|
||||
import { userName } from "../../stores/profile";
|
||||
import { profile as t, actions as tActions } from "../../translations";
|
||||
import { PrimalUser } from "../../types/primal";
|
||||
import ArticlePreview from "../ArticlePreview/ArticlePreview";
|
||||
import Avatar from "../Avatar/Avatar";
|
||||
import ButtonCopy from "../Buttons/ButtonCopy";
|
||||
import Loader from "../Loader/Loader";
|
||||
@ -133,6 +134,9 @@ const ProfileTabs: Component<{
|
||||
setCurrentTab(() => value);
|
||||
|
||||
switch(value) {
|
||||
case 'articles':
|
||||
profile.articles.length === 0 && profile.actions.fetchArticles(profile.profileKey);
|
||||
break;
|
||||
case 'notes':
|
||||
profile.notes.length === 0 && profile.actions.fetchNotes(profile.profileKey);
|
||||
break;
|
||||
@ -161,6 +165,18 @@ const ProfileTabs: Component<{
|
||||
>
|
||||
<Tabs.Root onChange={onChangeValue}>
|
||||
<Tabs.List class={styles.profileTabs}>
|
||||
<Show when={(profile?.userStats.long_form_note_count || 0) > 0}>
|
||||
<Tabs.Trigger class={styles.profileTab} value="articles">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
{humanizeNumber(profile?.userStats?.long_form_note_count || 0)}
|
||||
</div>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.articles)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
<Tabs.Trigger class={styles.profileTab} value="notes">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
@ -231,6 +247,62 @@ const ProfileTabs: Component<{
|
||||
<Tabs.Indicator class={styles.profileTabIndicator} />
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="articles">
|
||||
<div class={styles.profileNotes}>
|
||||
<Switch
|
||||
fallback={
|
||||
<div class={styles.loader}>
|
||||
<Loader />
|
||||
</div>
|
||||
}>
|
||||
<Match when={isMuted(profile?.profileKey)}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.isMuted,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
<button
|
||||
onClick={unMuteProfile}
|
||||
>
|
||||
{intl.formatMessage(tActions.unmute)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={isFiltered()}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(t.isFiltered)}
|
||||
<button
|
||||
onClick={addToAllowlist}
|
||||
>
|
||||
{intl.formatMessage(tActions.addToAllowlist)}
|
||||
</button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.articles.length === 0 && !profile.isFetching}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.noNotes,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.articles.length > 0}>
|
||||
<For each={profile?.articles}>
|
||||
{article => (
|
||||
<ArticlePreview article={article} />
|
||||
)}
|
||||
</For>
|
||||
<Paginator
|
||||
loadNextPage={() => {
|
||||
profile?.actions.fetchNextArticlesPage();
|
||||
}}
|
||||
isSmall={true}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="notes">
|
||||
<div class={styles.profileNotes}>
|
||||
<Switch
|
||||
|
@ -9,6 +9,32 @@
|
||||
}
|
||||
|
||||
.selectionIcon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
display: inline-block;
|
||||
margin-inline: 4px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/caret.svg) no-repeat 0 0/ 10px 10px;
|
||||
mask: url(../../assets/icons/caret.svg) no-repeat 0 0/ 10px 10px;
|
||||
}
|
||||
|
||||
.selectionBox {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
background-color: var(--background-site);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.selectionIconBig {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-block;
|
||||
@ -18,11 +44,7 @@
|
||||
mask: url(../../assets/icons/caret.svg) no-repeat 0 / 14px;
|
||||
}
|
||||
|
||||
.selectionBox {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
.triggerBig {
|
||||
background-color: var(--background-site);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -18,6 +18,7 @@ const SelectionBox: Component<{
|
||||
options: SelectionOption[],
|
||||
onChange: (option: any) => void,
|
||||
initialValue?: string | SelectionOption,
|
||||
big?: boolean,
|
||||
value?: string | SelectionOption,
|
||||
id?: string,
|
||||
}> = (props) => {
|
||||
@ -55,12 +56,12 @@ const SelectionBox: Component<{
|
||||
onChange={props.onChange}
|
||||
gutter={8}
|
||||
>
|
||||
<Select.Trigger class={styles.trigger}>
|
||||
<Select.Trigger class={props.big ? styles.triggerBig : styles.trigger}>
|
||||
<Select.Value<SelectionOption>>
|
||||
{state => state.selectedOption()?.label || ''}
|
||||
</Select.Value>
|
||||
<Select.Icon>
|
||||
<div class={styles.selectionIcon}></div>
|
||||
<div class={props.big ? styles.selectionIconBig : styles.selectionIcon}></div>
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
|
@ -0,0 +1,290 @@
|
||||
.subscribeToAuthor {
|
||||
position: fixed;
|
||||
min-width: 472px;
|
||||
min-height: 344px;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--background-input);
|
||||
border-radius: 8px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 24px 28px 24px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.title {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.userInfo {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.userData {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.userName {
|
||||
display: flex;
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nip05 {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
margin: 0px 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
mask: url(../../assets/icons/close.svg) no-repeat center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modalBody {
|
||||
.tiers {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
min-height: 220px;
|
||||
|
||||
.tier {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--subtile-devider);
|
||||
border-radius: 8px;
|
||||
background: none;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
width: 400px;
|
||||
|
||||
.tierTitle {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--subtile-devider);
|
||||
padding-top: 16px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.perks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
|
||||
.perk {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
// &::before {
|
||||
// content: '•';
|
||||
// }
|
||||
|
||||
.text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
background-color: var(--success-color);
|
||||
-webkit-mask: url(../../assets/icons/check.svg) no-repeat 0 / 80%;
|
||||
mask: url(../../assets/icons/check.svg) no-repeat 0 / 80%;
|
||||
|
||||
&.left {
|
||||
margin-right: 8px;
|
||||
}
|
||||
&.right {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.footer {
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
|
||||
.mint {
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0.15px;
|
||||
}
|
||||
|
||||
.payAction {
|
||||
height: 36px;
|
||||
min-width: 120px;
|
||||
button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zapIcon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-block;
|
||||
margin-right: 9px;
|
||||
background: var(--sidebar-section-icon-gradient);
|
||||
-webkit-mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
|
||||
mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
|
||||
}
|
||||
|
||||
.selectCosts {
|
||||
}
|
||||
|
||||
.selectTrigger {
|
||||
background-color: var(--background-input);
|
||||
width: 360px;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.selectValue {
|
||||
.cost {
|
||||
.duration {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
.chevIcon {
|
||||
width: 6px;
|
||||
height: 16px;
|
||||
background-color: var(--text-tertiary);
|
||||
-webkit-mask: url(../../assets/icons/chevron_right.svg) no-repeat 0 / 100%;
|
||||
mask: url(../../assets/icons/chevron_right.svg) no-repeat 0 / 100%;
|
||||
rotate: 90deg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.selectContent {
|
||||
background-color: var(--background-sheet);
|
||||
z-index: 9999;
|
||||
width: 390px;
|
||||
|
||||
.selectListbox {
|
||||
border: 1px solid var(--subtile-devider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--background-sheet);
|
||||
padding: 0;
|
||||
|
||||
.cost {
|
||||
border-radius: 8px;
|
||||
padding-block: 12px;
|
||||
padding-inline: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cost {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.duration {
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.noTiers {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
text-transform: uppercase;
|
||||
}
|
383
src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx
Normal file
383
src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx
Normal file
@ -0,0 +1,383 @@
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
|
||||
import { Component, createEffect, For, Show } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { Kind } from '../../constants';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { NostrTier, PrimalUser } from '../../types/primal';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import Modal from '../Modal/Modal';
|
||||
|
||||
import styles from './SubscribeToAuthorModal.module.scss';
|
||||
import { userName } from '../../stores/profile';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
||||
import { APP_ID } from '../../App';
|
||||
import { subsTo, subTo } from '../../sockets';
|
||||
import { getAuthorSubscriptionTiers } from '../../lib/feed';
|
||||
import ButtonSecondary from '../Buttons/ButtonSecondary';
|
||||
import { Select } from '@kobalte/core';
|
||||
import Loader from '../Loader/Loader';
|
||||
import { logInfo } from '../../lib/logger';
|
||||
import { getExchangeRate, getMembershipStatus } from '../../lib/membership';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
|
||||
|
||||
export const satsInBTC = 100_000_000;
|
||||
|
||||
export type TierCost = {
|
||||
amount: string,
|
||||
unit: string,
|
||||
cadence: string,
|
||||
id: string,
|
||||
}
|
||||
|
||||
export type Tier = {
|
||||
title: string,
|
||||
content: string,
|
||||
id: string,
|
||||
perks: string[],
|
||||
costs: TierCost[],
|
||||
activeCost: TierCost | undefined,
|
||||
client: string,
|
||||
event: NostrTier,
|
||||
};
|
||||
|
||||
export type TierStore = {
|
||||
tiers: Tier[],
|
||||
selectedTier: Tier | undefined,
|
||||
selectedCost: TierCost | undefined,
|
||||
isFetchingTiers: boolean,
|
||||
exchangeRate: Record<string, Record<string, number>>,
|
||||
}
|
||||
|
||||
export const payUnits = ['sats', 'sat', 'msat', 'msats', 'USD', 'usd', ''];
|
||||
|
||||
const SubscribeToAuthorModal: Component<{
|
||||
id?: string,
|
||||
author: PrimalUser | undefined,
|
||||
onClose: () => void,
|
||||
onSubscribe: (tier: Tier, cost: TierCost, exchangeRate?: Record<string, Record<string, number>>) => void,
|
||||
}> = (props) => {
|
||||
|
||||
const account = useAccountContext();
|
||||
|
||||
const [store, updateStore] = createStore<TierStore>({
|
||||
tiers: [],
|
||||
selectedTier: undefined,
|
||||
selectedCost: undefined,
|
||||
isFetchingTiers: false,
|
||||
exchangeRate: {},
|
||||
});
|
||||
|
||||
let walletSocket: WebSocket | undefined;
|
||||
|
||||
createEffect(() => {
|
||||
const author = props.author;
|
||||
|
||||
if (author) {
|
||||
getTiers(author);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (props.author && (!walletSocket || walletSocket.readyState === WebSocket.CLOSED)) {
|
||||
openWalletSocket(() => {
|
||||
if (!walletSocket || walletSocket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
const subId = `er_${APP_ID}`;
|
||||
|
||||
const unsub = subTo(walletSocket, subId, (type, _, content) => {
|
||||
if (type === 'EVENT') {
|
||||
const response: { rate: string } = JSON.parse(content?.content || '{ "rate": 1 }');
|
||||
|
||||
const BTCForTarget = parseFloat(response.rate) || 1;
|
||||
|
||||
const satsToTarget = BTCForTarget / satsInBTC;
|
||||
const targetToBTC = 1 / BTCForTarget;
|
||||
const targetToSats = 1 / satsToTarget;
|
||||
|
||||
updateStore('exchangeRate', () => ({
|
||||
USD: {
|
||||
sats: targetToSats,
|
||||
BTC: targetToBTC,
|
||||
USD: 1,
|
||||
},
|
||||
sats: {
|
||||
sats: 1,
|
||||
USD: satsToTarget,
|
||||
BTC: 1 / satsInBTC,
|
||||
},
|
||||
BTC: {
|
||||
sats: satsInBTC,
|
||||
USD: BTCForTarget,
|
||||
BTC: 1,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (type === 'EOSE') {
|
||||
unsub();
|
||||
walletSocket?.close();
|
||||
}
|
||||
});
|
||||
|
||||
getExchangeRate(account?.publicKey, subId, "USD", walletSocket);
|
||||
});
|
||||
} else {
|
||||
walletSocket?.close();
|
||||
}
|
||||
})
|
||||
|
||||
const getTiers = (author: PrimalUser) => {
|
||||
if (!author) return;
|
||||
|
||||
const subId = `subscription_tiers_${APP_ID}`;
|
||||
|
||||
let tiers: Tier[] = [];
|
||||
|
||||
const unsub = subsTo(subId, {
|
||||
onEvent: (_, content) => {
|
||||
if (content.kind === Kind.TierList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.Tier) {
|
||||
const t = content as NostrTier;
|
||||
|
||||
let costs = t.tags?.filter((t: string[]) => t[0] === 'amount').map((t: string[]) => (
|
||||
{
|
||||
amount: t[1],
|
||||
unit: t[2],
|
||||
cadence: t[3],
|
||||
id: `${t[1]}_${t[2]}_${t[3]}`
|
||||
})) || [];
|
||||
|
||||
const tier = {
|
||||
title: (t.tags?.find((t: string[]) => t[0] === 'title') || [])[1] || t.content || '',
|
||||
id: t.id || '',
|
||||
content: t.content || '',
|
||||
perks: t.tags?.filter((t: string[]) => t[0] === 'perk').map((t: string[]) => t[1]) || [],
|
||||
costs,
|
||||
client: (t.tags?.find((t: string[]) => t[0] === 'client') || [])[1] || t.content || '',
|
||||
event: t,
|
||||
activeCost: costs[0],
|
||||
}
|
||||
|
||||
tiers.push(tier)
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
onEose: () => {
|
||||
unsub();
|
||||
updateStore('isFetchingTiers', () => false);
|
||||
updateStore('tiers', () => [...tiers]);
|
||||
const tier: Tier | undefined = tiers.length > 0 ? Object.assign(tiers[0]) : undefined;
|
||||
updateStore('selectedTier', () => tier ? ({ ...tier }) : undefined);
|
||||
updateStore('selectedCost', () => tier ? ({ ...tier?.costs[0] }) : undefined);
|
||||
},
|
||||
})
|
||||
|
||||
updateStore('isFetchingTiers', () => true);
|
||||
getAuthorSubscriptionTiers(author.pubkey, subId)
|
||||
}
|
||||
|
||||
const selectTier = (tier: Tier) => {
|
||||
if (tier.id !== store.selectedTier?.id) {
|
||||
updateStore('selectedTier', () => ({ ...tier }));
|
||||
updateStore('selectedCost', (sc) => ({ ...costOptions(tier)[0] }) );
|
||||
}
|
||||
}
|
||||
|
||||
const isSelectedTier = (tier: Tier) => tier.id === store.selectedTier?.id;
|
||||
|
||||
|
||||
const costOptions = (tier: Tier) => {
|
||||
return tier.costs.filter(cost => payUnits.includes(cost.unit));
|
||||
}
|
||||
|
||||
const displayCost = (cost: TierCost | undefined) => {
|
||||
let text = '';
|
||||
|
||||
switch(cost?.unit) {
|
||||
case 'msat':
|
||||
case 'msats':
|
||||
case '':
|
||||
text = `${Math.ceil(parseInt(cost?.amount || '0') / 1_000)} sats`;
|
||||
break;
|
||||
case 'sats':
|
||||
case 'sat':
|
||||
text = `${cost.amount} sats`;
|
||||
break;
|
||||
case 'USD':
|
||||
case 'usd':
|
||||
text = `${cost.amount} USD`;
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
const openWalletSocket = (onOpen: () => void) => {
|
||||
walletSocket = new WebSocket('wss://wallet.primal.net/v1');
|
||||
|
||||
walletSocket.addEventListener('close', () => {
|
||||
logInfo('WALLET SOCKET CLOSED');
|
||||
});
|
||||
|
||||
walletSocket.addEventListener('open', () => {
|
||||
logInfo('WALLET SOCKET OPENED');
|
||||
onOpen();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={props.author !== undefined} onClose={props.onClose}>
|
||||
<div id={props.id} class={styles.subscribeToAuthor}>
|
||||
<div class={styles.header}>
|
||||
<div class={styles.userInfo}>
|
||||
<Avatar user={props.author} />
|
||||
<div class={styles.userData}>
|
||||
<div class={styles.userName}>
|
||||
{userName(props.author)}
|
||||
<VerificationCheck user={props.author} />
|
||||
</div>
|
||||
<div class={styles.nip05}>
|
||||
{props.author?.nip05}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class={styles.close} onClick={props.onClose}>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class={styles.modalBody}>
|
||||
<div class={styles.tiers}>
|
||||
<Show
|
||||
when={!store.isFetchingTiers}
|
||||
fallback={<div><Loader/></div>}
|
||||
>
|
||||
<For
|
||||
each={store.tiers}
|
||||
fallback={
|
||||
<div class={styles.noTiers}>
|
||||
No compatible tiers found
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(tier) => (
|
||||
<button
|
||||
class={`${styles.tier} ${isSelectedTier(tier) ? styles.selected : ''}`}
|
||||
onClick={() => selectTier(tier)}
|
||||
>
|
||||
<div class={styles.tierTitle}>{tier.title}</div>
|
||||
|
||||
<Show
|
||||
when={costOptions(tier).length > 1 && store.selectedTier?.id === tier.id}
|
||||
fallback={<div class={styles.cost}>
|
||||
<div class={styles.amount}>
|
||||
{displayCost(costOptions(tier)[0])}
|
||||
</div>
|
||||
<div class={styles.duration}>
|
||||
{costOptions(tier)[0].cadence}
|
||||
</div>
|
||||
</div>}
|
||||
>
|
||||
<Select.Root
|
||||
class={styles.selectCosts}
|
||||
options={costOptions(tier)}
|
||||
optionValue="id"
|
||||
value={store.selectedCost}
|
||||
onChange={(cost) => {
|
||||
// updateStore('tiers', index(), 'activeCost', () => ({ ...cost }));
|
||||
// updateStore('selectedTier', 'activeCost', () => ({ ...cost }));
|
||||
updateStore('selectedCost', () => ({ ...cost }));
|
||||
}}
|
||||
itemComponent={props => (
|
||||
<Select.Item item={props.item} class={styles.cost}>
|
||||
<div class={styles.amount}>
|
||||
{displayCost(props.item.rawValue)}
|
||||
</div>
|
||||
<div class={styles.duration}>
|
||||
{props.item.rawValue.cadence}
|
||||
</div>
|
||||
</Select.Item>
|
||||
)}
|
||||
>
|
||||
<Select.Trigger class={styles.selectTrigger}>
|
||||
<Select.Value class={styles.selectValue}>
|
||||
{state => {
|
||||
const cost = state.selectedOption() as TierCost;
|
||||
|
||||
return (
|
||||
<div class={styles.cost}>
|
||||
<div class={styles.amount}>
|
||||
{displayCost(cost)}
|
||||
</div>
|
||||
<div class={styles.duration}>
|
||||
<div>{cost?.cadence}</div>
|
||||
<div class={styles.chevIcon}></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}}
|
||||
</Select.Value>
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content class={styles.selectContent}>
|
||||
<Select.Listbox class={styles.selectListbox} />
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
|
||||
</Show>
|
||||
|
||||
<div class={styles.content}>
|
||||
{tier.content}
|
||||
</div>
|
||||
<div class={styles.perks}>
|
||||
<For each={tier.perks}>
|
||||
{perk => (
|
||||
<div class={styles.perk}>
|
||||
<div class={styles.checkIcon}></div>
|
||||
<div class={styles.text}>{perk}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.footer}>
|
||||
<div class={styles.payAction}>
|
||||
<ButtonSecondary
|
||||
light={true}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
cancel
|
||||
</ButtonSecondary>
|
||||
</div>
|
||||
|
||||
<Show when={store.selectedTier}>
|
||||
<div class={styles.payAction}>
|
||||
<ButtonPrimary
|
||||
onClick={() => store.selectedTier && store.selectedCost && props.onSubscribe(store.selectedTier, store.selectedCost, store.exchangeRate)}
|
||||
>
|
||||
subscribe
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(SubscribeToAuthorModal);
|
@ -102,16 +102,19 @@ export enum Kind {
|
||||
ChannelHideMessage = 43,
|
||||
ChannelMuteUser = 44,
|
||||
|
||||
LongForm = 30_023,
|
||||
|
||||
Subscribe = 7_001,
|
||||
Unsubscribe = 7_002,
|
||||
Zap = 9_735,
|
||||
|
||||
MuteList = 10_000,
|
||||
RelayList = 10_002,
|
||||
Bookmarks = 10_003,
|
||||
CategorizedPeople = 30_000,
|
||||
TierList = 17_000,
|
||||
|
||||
CategorizedPeople = 30_000,
|
||||
LongForm = 30_023,
|
||||
Settings = 30_078,
|
||||
Tier = 37_001,
|
||||
|
||||
ACK = 10_000_098,
|
||||
NoteStats = 10_000_100,
|
||||
@ -143,6 +146,7 @@ export enum Kind {
|
||||
RelayHint=10_000_141,
|
||||
NoteQuoteStats=10_000_143,
|
||||
WordCount=10_000_144,
|
||||
FeaturedAuthors=10_000_148,
|
||||
|
||||
WALLET_OPERATION = 10_000_300,
|
||||
}
|
||||
|
@ -9,6 +9,9 @@ import {
|
||||
} from "solid-js";
|
||||
import { PrimalArticle, PrimalNote, PrimalUser, ZapOption } from "../types/primal";
|
||||
import { CashuMint } from "@cashu/cashu-ts";
|
||||
import { Tier, TierCost } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
|
||||
import { Kind } from "../constants";
|
||||
import { sendEvent } from "../lib/notes";
|
||||
|
||||
|
||||
export type ReactionStats = {
|
||||
@ -29,7 +32,7 @@ export type CustomZapInfo = {
|
||||
};
|
||||
|
||||
export type NoteContextMenuInfo = {
|
||||
note: PrimalNote,
|
||||
note: PrimalNote | PrimalArticle,
|
||||
position: DOMRect | undefined,
|
||||
openCustomZap?: () => void,
|
||||
openReactions?: () => void,
|
||||
@ -66,13 +69,15 @@ export type AppContextStore = {
|
||||
showConfirmModal: boolean,
|
||||
confirmInfo: ConfirmInfo | undefined,
|
||||
cashuMints: Map<string, CashuMint>,
|
||||
subscribeToAuthor: PrimalUser | undefined,
|
||||
subscribeToTier: (tier: Tier) => void,
|
||||
actions: {
|
||||
openReactionModal: (noteId: string, stats: ReactionStats) => void,
|
||||
closeReactionModal: () => void,
|
||||
openCustomZapModal: (custonZapInfo: CustomZapInfo) => void,
|
||||
closeCustomZapModal: () => void,
|
||||
resetCustomZap: () => void,
|
||||
openContextMenu: (note: PrimalNote, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
|
||||
openContextMenu: (note: PrimalNote | PrimalArticle, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
|
||||
closeContextMenu: () => void,
|
||||
openLnbcModal: (lnbc: string, onPay: () => void) => void,
|
||||
closeLnbcModal: () => void,
|
||||
@ -81,6 +86,8 @@ export type AppContextStore = {
|
||||
openConfirmModal: (confirmInfo: ConfirmInfo) => void,
|
||||
closeConfirmModal: () => void,
|
||||
getCashuMint: (url: string) => CashuMint | undefined,
|
||||
openAuthorSubscribeModal: (author: PrimalUser | undefined, subscribeTo: (tier: Tier, cost: TierCost) => void) => void,
|
||||
closeAuthorSubscribeModal: () => void,
|
||||
},
|
||||
}
|
||||
|
||||
@ -106,6 +113,8 @@ const initialData: Omit<AppContextStore, 'actions'> = {
|
||||
showConfirmModal: false,
|
||||
confirmInfo: undefined,
|
||||
cashuMints: new Map(),
|
||||
subscribeToAuthor: undefined,
|
||||
subscribeToTier: () => {},
|
||||
};
|
||||
|
||||
export const AppContext = createContext<AppContextStore>();
|
||||
@ -155,7 +164,7 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
};
|
||||
|
||||
const openContextMenu = (
|
||||
note: PrimalNote,
|
||||
note: PrimalNote | PrimalArticle,
|
||||
position: DOMRect | undefined,
|
||||
openCustomZap: () => void,
|
||||
openReactions: () => void,
|
||||
@ -221,6 +230,17 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
return store.cashuMints.get(formatted);
|
||||
};
|
||||
|
||||
const openAuthorSubscribeModal = (author: PrimalUser | undefined, subscribeTo: (tier: Tier, cost: TierCost) => void) => {
|
||||
if (!author) return;
|
||||
|
||||
updateStore('subscribeToAuthor', () => ({ ...author }));
|
||||
updateStore('subscribeToTier', () => subscribeTo);
|
||||
};
|
||||
|
||||
const closeAuthorSubscribeModal = () => {
|
||||
updateStore('subscribeToAuthor', () => undefined);
|
||||
};
|
||||
|
||||
// EFFECTS --------------------------------------
|
||||
|
||||
onMount(() => {
|
||||
@ -273,6 +293,8 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
openCashuModal,
|
||||
closeCashuModal,
|
||||
getCashuMint,
|
||||
openAuthorSubscribeModal,
|
||||
closeAuthorSubscribeModal,
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
NostrUserContent,
|
||||
NostrUserZaps,
|
||||
NoteActions,
|
||||
PrimalArticle,
|
||||
PrimalNote,
|
||||
PrimalUser,
|
||||
PrimalZap,
|
||||
@ -52,6 +53,7 @@ import { setLinkPreviews } from "../lib/notes";
|
||||
import { subscribeTo } from "../sockets";
|
||||
import { parseBolt11 } from "../utils";
|
||||
import { readRecomendedUsers, saveRecomendedUsers } from "../lib/localStore";
|
||||
import { fetchUserArticles } from "../handleNotes";
|
||||
|
||||
export type UserStats = {
|
||||
pubkey: string,
|
||||
@ -63,6 +65,7 @@ export type UserStats = {
|
||||
total_zap_count: number,
|
||||
total_satszapped: number,
|
||||
relay_count: number,
|
||||
long_form_note_count?: number,
|
||||
};
|
||||
|
||||
export type ProfileContextStore = {
|
||||
@ -71,6 +74,7 @@ export type ProfileContextStore = {
|
||||
userStats: UserStats,
|
||||
fetchedUserStats: boolean,
|
||||
knownProfiles: VanityProfiles,
|
||||
articles: PrimalArticle[],
|
||||
notes: PrimalNote[],
|
||||
replies: PrimalNote[],
|
||||
zaps: PrimalZap[],
|
||||
@ -91,6 +95,7 @@ export type ProfileContextStore = {
|
||||
repliesPage: FeedPage,
|
||||
reposts: Record<string, string> | undefined,
|
||||
lastNote: PrimalNote | undefined,
|
||||
lastArticle: PrimalArticle | undefined,
|
||||
lastReply: PrimalNote | undefined,
|
||||
following: string[],
|
||||
sidebar: FeedPage & { notes: PrimalNote[] },
|
||||
@ -118,6 +123,9 @@ export type ProfileContextStore = {
|
||||
fetchNextRepliesPage: () => void,
|
||||
fetchNotes: (noteId: string | undefined, until?: number) => void,
|
||||
fetchNextPage: () => void,
|
||||
fetchArticles: (noteId: string | undefined, until?: number) => void,
|
||||
fetchNextArticlesPage: () => void,
|
||||
clearArticles: () => void,
|
||||
updatePage: (content: NostrEventContent) => void,
|
||||
savePage: (page: FeedPage) => void,
|
||||
updateRepliesPage: (content: NostrEventContent) => void,
|
||||
@ -156,6 +164,7 @@ export const initialData = {
|
||||
userStats: { ...emptyStats },
|
||||
fetchedUserStats: false,
|
||||
knownProfiles: { names: {} },
|
||||
articles: [],
|
||||
notes: [],
|
||||
replies: [],
|
||||
isFetching: false,
|
||||
@ -170,6 +179,7 @@ export const initialData = {
|
||||
zappers: {},
|
||||
zapListOffset: 0,
|
||||
lastNote: undefined,
|
||||
lastArticle: undefined,
|
||||
lastReply: undefined,
|
||||
lastZap: undefined,
|
||||
following: [],
|
||||
@ -465,6 +475,20 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
updateStore('isFetchingReplies', () => false);
|
||||
};
|
||||
|
||||
const fetchArticles = async (pubkey: string | undefined, until = 0, limit = 20) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('isFetching', () => true);
|
||||
let articles = await fetchUserArticles(account?.publicKey, pubkey, 'authored', `profile_articles_${APP_ID}`, until, limit);
|
||||
|
||||
articles = articles.filter(a => a.id !== store.lastArticle?.id);
|
||||
|
||||
updateStore('articles', (arts) => [ ...arts, ...articles]);
|
||||
updateStore('isFetching', () => false);
|
||||
}
|
||||
|
||||
const fetchNotes = (pubkey: string | undefined, until = 0, limit = 20) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
@ -499,6 +523,11 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const clearArticles = () => {
|
||||
updateStore('articles', () => []);
|
||||
updateStore('lastArticle', () => undefined);
|
||||
};
|
||||
|
||||
const clearReplies = () => {
|
||||
updateStore('repliesPage', () => ({ messages: [], users: {}, postStats: {}, noteActions: {} }));
|
||||
updateStore('replies', () => []);
|
||||
@ -545,6 +574,28 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchNextArticlesPage = () => {
|
||||
const lastArticle = store.articles[store.articles.length - 1];
|
||||
|
||||
if (!lastArticle) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('lastArticle', () => ({ ...lastArticle }));
|
||||
|
||||
const criteria = paginationPlan('latest');
|
||||
|
||||
const noteData: Record<string, any> = lastArticle.repost ?
|
||||
lastArticle.repost.note :
|
||||
lastArticle.msg;
|
||||
|
||||
const until = noteData[criteria];
|
||||
|
||||
if (until > 0 && store.profileKey) {
|
||||
fetchArticles(store.profileKey, until);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchNextRepliesPage = () => {
|
||||
const lastReply = store.replies[store.replies.length - 1];
|
||||
|
||||
@ -1357,6 +1408,9 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
clearNotes,
|
||||
fetchNotes,
|
||||
fetchNextPage,
|
||||
fetchArticles,
|
||||
fetchNextArticlesPage,
|
||||
clearArticles,
|
||||
updatePage,
|
||||
savePage,
|
||||
saveReplies,
|
||||
|
@ -2,11 +2,11 @@ import { nip19 } from "nostr-tools";
|
||||
import { createContext, createEffect, onCleanup, useContext } from "solid-js";
|
||||
import { createStore, reconcile, unwrap } from "solid-js/store";
|
||||
import { APP_ID } from "../App";
|
||||
import { Kind } from "../constants";
|
||||
import { Kind, minKnownProfiles } from "../constants";
|
||||
import { getArticlesFeed, getEvents, getExploreFeed, getFeed, getFutureArticlesFeed, getFutureExploreFeed, getFutureFeed } from "../lib/feed";
|
||||
import { fetchStoredFeed, saveStoredFeed } from "../lib/localStore";
|
||||
import { setLinkPreviews } from "../lib/notes";
|
||||
import { getScoredUsers, searchContent } from "../lib/search";
|
||||
import { getRecomendedArticleIds, getScoredUsers, searchContent } from "../lib/search";
|
||||
import { isConnected, refreshSocketListeners, removeSocketListeners, socket } from "../sockets";
|
||||
import { sortingPlan, convertToNotes, parseEmptyReposts, paginationPlan, isInTags, isRepostInCollection, convertToArticles, isLFRepostInCollection } from "../stores/note";
|
||||
import {
|
||||
@ -41,6 +41,7 @@ type ReadsContextStore = {
|
||||
lastNote: PrimalArticle | undefined,
|
||||
reposts: Record<string, string> | undefined,
|
||||
mentionedNotes: Record<string, NostrNoteContent>,
|
||||
recomendedReads: string[],
|
||||
future: {
|
||||
notes: PrimalArticle[],
|
||||
page: FeedPage,
|
||||
@ -119,6 +120,7 @@ const initialHomeData = {
|
||||
isFetching: false,
|
||||
query: undefined,
|
||||
},
|
||||
recomendedReads: [],
|
||||
};
|
||||
|
||||
export const ReadsContext = createContext<ReadsContextStore>();
|
||||
@ -201,13 +203,13 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
};
|
||||
|
||||
const doSidebarSearch = (query: string) => {
|
||||
const subid = `reads_sidebar_${APP_ID}`;
|
||||
const subid = `reads_recomended_${APP_ID}`;
|
||||
|
||||
updateStore('sidebar', 'isFetching', () => true);
|
||||
updateStore('sidebar', 'notes', () => []);
|
||||
updateStore('sidebar', 'page', { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {} });
|
||||
updateStore('sidebar', 'page', { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {}, topZaps: {} });
|
||||
|
||||
getScoredUsers(account?.publicKey, query, 10, subid);
|
||||
getRecomendedArticleIds(subid);
|
||||
}
|
||||
|
||||
const clearFuture = () => {
|
||||
@ -307,7 +309,8 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
};
|
||||
|
||||
const fetchNotes = (topic: string, subId: string, until = 0, includeReplies?: boolean) => {
|
||||
const [scope, timeframe] = topic.split(';');
|
||||
const t = topic === 'none' ? '' : topic;//account?.publicKey || '532d830dffe09c13e75e8b145c825718fc12b0003f61d61e9077721c7fff93cb';
|
||||
const [scope, timeframe] = t.split(';');
|
||||
|
||||
updateStore('isFetching', true);
|
||||
updateStore('page', () => ({ messages: [], users: {}, postStats: {} }));
|
||||
@ -329,7 +332,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
getArticlesFeed(account?.publicKey, topic, `reads_feed_${subId}`, until, 20);
|
||||
getArticlesFeed(account?.publicKey, t, `reads_feed_${subId}`, until, 20);
|
||||
};
|
||||
|
||||
const clearNotes = () => {
|
||||
@ -391,7 +394,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
const selectFeed = (feed: PrimalFeed | undefined) => {
|
||||
if (feed?.hex !== undefined && (feed.hex !== currentFeed?.hex || feed.includeReplies !== currentFeed?.includeReplies)) {
|
||||
currentFeed = { ...feed };
|
||||
saveStoredFeed(account?.publicKey, currentFeed);
|
||||
// saveStoredFeed(account?.publicKey, currentFeed);
|
||||
|
||||
updateStore('selectedFeed', reconcile({...feed}));
|
||||
clearNotes();
|
||||
@ -489,7 +492,6 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
if (content.kind === Kind.NoteStats) {
|
||||
const statistic = content as NostrStatsContent;
|
||||
const stat = JSON.parse(statistic.content);
|
||||
console.log('READS STATS: ', stat)
|
||||
|
||||
if (scope) {
|
||||
updateStore(scope, 'page', 'postStats',
|
||||
@ -524,7 +526,6 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
const noteActionContent = content as NostrNoteActionsContent;
|
||||
const noteActions = JSON.parse(noteActionContent.content) as NoteActions;
|
||||
|
||||
console.log('READS ACTIONS: ', content)
|
||||
if (scope) {
|
||||
updateStore(scope, 'page', 'noteActions',
|
||||
(actions) => ({ ...actions, [noteActions.event_id]: { ...noteActions } })
|
||||
@ -646,6 +647,27 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
|
||||
const [type, subId, content] = message;
|
||||
|
||||
if (subId === `reads_recomended_${APP_ID}`) {
|
||||
if (type === 'EOSE') {
|
||||
// saveSidebarPage(store.sidebar.page);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (type === 'EVENT') {
|
||||
const recomended = JSON.parse(content?.content || '{}');
|
||||
const ids = recomended.reads.reduce((acc: string[], r: string[]) => r[0] ? [ ...acc, r[0] ] : acc, []);
|
||||
|
||||
updateStore('recomendedReads', () => [ ...ids ])
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (subId === `reads_sidebar_${APP_ID}`) {
|
||||
if (type === 'EOSE') {
|
||||
saveSidebarPage(store.sidebar.page);
|
||||
@ -771,9 +793,9 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (account?.isKeyLookupDone && settings?.defaultFeed) {
|
||||
const storedFeed = fetchStoredFeed(account.publicKey);
|
||||
selectFeed(storedFeed || settings?.defaultFeed);
|
||||
if (account?.isKeyLookupDone && account.publicKey) {
|
||||
|
||||
selectFeed({ hex: account.publicKey, name: 'My Reads'});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,20 +1,21 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Kind } from "./constants";
|
||||
import { getEvents } from "./lib/feed";
|
||||
import { setLinkPreviews } from "./lib/notes";
|
||||
import { getEvents, getUserArticleFeed } from "./lib/feed";
|
||||
import { decodeIdentifier, hexToNpub } from "./lib/keys";
|
||||
import { getParametrizedEvents, setLinkPreviews } from "./lib/notes";
|
||||
import { getUserProfileInfo } from "./lib/profile";
|
||||
import { updateStore, store } from "./services/StoreService";
|
||||
import { subscribeTo } from "./sockets";
|
||||
import { convertToNotes } from "./stores/note";
|
||||
import { convertToArticles, convertToNotes } from "./stores/note";
|
||||
import { convertToUser } from "./stores/profile";
|
||||
import { account } from "./translations";
|
||||
import { FeedPage, NostrEventContent, NostrEventType, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalNote, TopZap } from "./types/primal";
|
||||
import { EventCoordinate, FeedPage, NostrEventContent, NostrEventType, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalArticle, PrimalNote, PrimalUser, TopZap } from "./types/primal";
|
||||
import { parseBolt11 } from "./utils";
|
||||
|
||||
export const fetchNotes = (pubkey: string | undefined, noteIds: string[], subId: string) => {
|
||||
return new Promise<PrimalNote[]>((resolve, reject) => {
|
||||
if (!pubkey) reject('Missing pubkey');
|
||||
|
||||
let note: PrimalNote;
|
||||
|
||||
let page: FeedPage = {
|
||||
users: {},
|
||||
messages: [],
|
||||
@ -182,3 +183,601 @@ export const fetchNotes = (pubkey: string | undefined, noteIds: string[], subId:
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchArticles = (noteIds: string[], subId: string) => {
|
||||
return new Promise<PrimalArticle[]>((resolve, reject) => {
|
||||
|
||||
let page: FeedPage = {
|
||||
users: {},
|
||||
messages: [],
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
relayHints: {},
|
||||
topZaps: {},
|
||||
since: 0,
|
||||
until: 0,
|
||||
wordCount: {},
|
||||
}
|
||||
|
||||
const events = noteIds.reduce<EventCoordinate[]>((acc, id) => {
|
||||
const d = decodeIdentifier(id);
|
||||
|
||||
if (!d.data || d.type !== 'naddr') return acc;
|
||||
|
||||
const { pubkey, identifier, kind } = d.data;
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{ identifier, pubkey, kind },
|
||||
]
|
||||
|
||||
}, []);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
getParametrizedEvents(events, subId);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const fetchArticleThread = (pubkey: string | undefined, noteIds: string, 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 primaryArticle: PrimalArticle | undefined;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
getEvents(pubkey, [...noteIds], subId, true);
|
||||
|
||||
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.NoteQuoteStats) {
|
||||
const quoteStats = JSON.parse(content.content);
|
||||
|
||||
|
||||
// updateStore('quoteCount', () => quoteStats.count || 0);
|
||||
return;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const fetchUserArticles = (userPubkey: string | undefined, pubkey: string | undefined, type: 'authored' | 'replies' | 'bookmarks', subId: string, until = 0, limit = 10) => {
|
||||
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, until, limit);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchUserProfile = (userPubkey: string | undefined, pubkey: string | undefined, subId: string) => {
|
||||
return new Promise<PrimalUser>((resolve, reject) => {
|
||||
if (!pubkey) reject('Missing pubkey');
|
||||
|
||||
let user: PrimalUser | undefined;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, _, content) => {
|
||||
|
||||
if (type === 'EOSE') {
|
||||
unsub();
|
||||
user ? resolve(user) : reject('user not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (!content) return;
|
||||
updatePage(content);
|
||||
}
|
||||
});
|
||||
|
||||
getUserProfileInfo(pubkey, userPubkey, subId);
|
||||
|
||||
const updatePage = (content: NostrEventContent) => {
|
||||
if (content?.kind === Kind.Metadata) {
|
||||
let userData = JSON.parse(content.content);
|
||||
|
||||
if (!userData.displayName || typeof userData.displayName === 'string' && userData.displayName.trim().length === 0) {
|
||||
userData.displayName = userData.display_name;
|
||||
}
|
||||
userData.pubkey = content.pubkey;
|
||||
userData.npub = hexToNpub(content.pubkey);
|
||||
userData.created_at = content.created_at;
|
||||
|
||||
user = { ...userData };
|
||||
return;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -62,10 +62,10 @@
|
||||
--warning-color: #FA3C3C;
|
||||
--success-color: #66E205;
|
||||
|
||||
--left-col-w: 188px;
|
||||
--left-col-w: 182px;
|
||||
--center-col-w: 640px;
|
||||
--right-col-w: 348px;
|
||||
--full-site-w: 1177px;
|
||||
--full-site-w: 1172px;
|
||||
--header-height: 84px;
|
||||
|
||||
background-color: var(--background-site);
|
||||
@ -144,7 +144,9 @@ body::after{
|
||||
url(./assets/icons/nav/settings.svg)
|
||||
url(./assets/icons/nav/settings_selected.svg)
|
||||
url(./assets/icons/nav/long.svg)
|
||||
url(./assets/icons/nav/long_selected.svg);
|
||||
url(./assets/icons/nav/long_selected.svg)
|
||||
url(./assets/images/reads_image_dark.png)
|
||||
url(./assets/images/reads_image_light.png);
|
||||
}
|
||||
|
||||
.reply_icon {
|
||||
|
@ -27,11 +27,12 @@ export const getFeed = (user_pubkey: string | undefined, pubkey: string | undef
|
||||
return;
|
||||
}
|
||||
|
||||
const start = until === 0 ? 'since' : 'until';
|
||||
const time = until === 0 ? Math.ceil((new Date()).getTime()/1_000 ): until;
|
||||
|
||||
let payload = { limit, [start]: until, pubkey };
|
||||
let payload = { limit, until: time, pubkey };
|
||||
|
||||
if (user_pubkey) {
|
||||
// @ts-ignore dynamic property
|
||||
payload.user_pubkey = user_pubkey;
|
||||
}
|
||||
if (include_replies) {
|
||||
@ -47,15 +48,21 @@ export const getFeed = (user_pubkey: string | undefined, pubkey: string | undef
|
||||
}
|
||||
|
||||
export const getArticlesFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, until = 0, limit = 20) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
// if (!pubkey) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const start = until === 0 ? 'since' : 'until';
|
||||
|
||||
let payload = { limit, [start]: until, pubkey };
|
||||
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;
|
||||
}
|
||||
|
||||
@ -134,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,
|
||||
@ -320,3 +355,37 @@ export const getMostZapped4h = (
|
||||
]},
|
||||
]));
|
||||
};
|
||||
|
||||
export const getReadsTopics = (
|
||||
subid: string,
|
||||
) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["get_reads_topics"]},
|
||||
]));
|
||||
};
|
||||
|
||||
|
||||
export const getFeaturedAuthors = (
|
||||
subid: string,
|
||||
) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["get_featured_authors"]},
|
||||
]));
|
||||
};
|
||||
|
||||
export const getAuthorSubscriptionTiers = (
|
||||
pubkey: string | undefined,
|
||||
subid: string,
|
||||
) => {
|
||||
if (!pubkey) return;
|
||||
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["creator_paid_tiers", { pubkey }]},
|
||||
]));
|
||||
};
|
||||
|
@ -36,3 +36,39 @@ export const getMembershipStatus = async (pubkey: string | undefined, subId: str
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const getExchangeRate = async (pubkey: string | undefined, subId: string, currency: string, socket: WebSocket) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = JSON.stringify(
|
||||
["exchange_rate", { target_currency: currency }],
|
||||
);
|
||||
|
||||
const event = {
|
||||
content,
|
||||
kind: Kind.WALLET_OPERATION,
|
||||
created_at: Math.ceil((new Date()).getTime() / 1000),
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const signedEvent = await signEvent(event);
|
||||
|
||||
const message = JSON.stringify([
|
||||
"REQ",
|
||||
subId,
|
||||
{cache: ["wallet", { operation_event: signedEvent }]},
|
||||
]);
|
||||
|
||||
if (socket) {
|
||||
const e = new CustomEvent('send', { detail: { message, ws: socket }});
|
||||
|
||||
socket.send(message);
|
||||
socket.dispatchEvent(e);
|
||||
} else {
|
||||
throw('no_socket');
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { createStore } from "solid-js/store";
|
||||
import LinkPreview from "../components/LinkPreview/LinkPreview";
|
||||
import { addrRegex, appleMusicRegex, emojiRegex, hashtagRegex, interpunctionRegex, Kind, linebreakRegex, lnRegex, lnUnifiedRegex, mixCloudRegex, nostrNestsRegex, noteRegex, noteRegexLocal, profileRegex, profileRegexG, soundCloudRegex, spotifyRegex, tagMentionRegex, twitchRegex, urlRegex, urlRegexG, wavlakeRegex, youtubeRegex } from "../constants";
|
||||
import { sendMessage, subscribeTo } from "../sockets";
|
||||
import { MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalArticle, PrimalNote, SendNoteResult } from "../types/primal";
|
||||
import { EventCoordinate, MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalArticle, PrimalNote, SendNoteResult } from "../types/primal";
|
||||
import { npubToHex } from "./keys";
|
||||
import { logError, logInfo, logWarning } from "./logger";
|
||||
import { getMediaUrl as getMediaUrlDefault } from "./media";
|
||||
@ -343,7 +343,7 @@ export const sendArticleRepost = async (note: PrimalArticle, relays: Relay[], re
|
||||
kind: Kind.Repost,
|
||||
tags: [
|
||||
['e', note.id],
|
||||
['p', note.author.pubkey],
|
||||
['p', note.pubkey],
|
||||
],
|
||||
created_at: Math.floor((new Date()).getTime() / 1000),
|
||||
};
|
||||
@ -615,3 +615,12 @@ export const getParametrizedEvent = (pubkey: string, identifier: string, kind: n
|
||||
{cache: ["parametrized_replaceable_event", { pubkey, kind, identifier, extended_response: true }]},
|
||||
]));
|
||||
};
|
||||
|
||||
|
||||
export const getParametrizedEvents = (events: EventCoordinate[], subid: string) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["parametrized_replaceable_events", { events, extended_response: true }]},
|
||||
]));
|
||||
};
|
||||
|
@ -75,3 +75,11 @@ export const getScoredUsers = (user_pubkey: string | undefined, selector: string
|
||||
{cache: ['scored', { user_pubkey, selector }]},
|
||||
]));
|
||||
};
|
||||
|
||||
export const getRecomendedArticleIds = (subid: string) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ['get_recommended_reads']},
|
||||
]));
|
||||
};
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { bech32 } from "@scure/base";
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
import { nip57, Relay, utils } from "nostr-tools";
|
||||
import { PrimalArticle, PrimalNote, PrimalUser } from "../types/primal";
|
||||
import { Tier } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
|
||||
import { Kind } from "../constants";
|
||||
import { NostrRelaySignedEvent, PrimalArticle, PrimalNote, PrimalUser } from "../types/primal";
|
||||
import { logError } from "./logger";
|
||||
import { enableWebLn, sendPayment, signEvent } from "./nostrAPI";
|
||||
|
||||
@ -19,8 +21,8 @@ export const zapNote = async (note: PrimalNote, sender: string | undefined, amou
|
||||
const sats = Math.round(amount * 1000);
|
||||
|
||||
let payload = {
|
||||
profile: note.post.pubkey,
|
||||
event: note.msg.id,
|
||||
profile: note.pubkey,
|
||||
event: note.id,
|
||||
amount: sats,
|
||||
relays: relays.map(r => r.url)
|
||||
};
|
||||
@ -55,7 +57,7 @@ export const zapArticle = async (note: PrimalArticle, sender: string | undefined
|
||||
return false;
|
||||
}
|
||||
|
||||
const callback = await getZapEndpoint(note.author);
|
||||
const callback = await getZapEndpoint(note.user);
|
||||
|
||||
if (!callback) {
|
||||
return false;
|
||||
@ -138,6 +140,67 @@ export const zapProfile = async (profile: PrimalUser, sender: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const zapSubscription = async (subEvent: NostrRelaySignedEvent, recipient: PrimalUser, sender: string | undefined, relays: Relay[], exchangeRate?: Record<string, Record<string, number>>) => {
|
||||
if (!sender || !recipient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const callback = await getZapEndpoint(recipient);
|
||||
|
||||
if (!callback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const costTag = subEvent.tags.find(t => t [0] === 'amount');
|
||||
if (!costTag) return false;
|
||||
|
||||
let sats = 0;
|
||||
|
||||
if (costTag[2] === 'sats') {
|
||||
sats = parseInt(costTag[1]) * 1_000;
|
||||
}
|
||||
|
||||
if (costTag[2] === 'msat') {
|
||||
sats = parseInt(costTag[1]);
|
||||
}
|
||||
|
||||
if (costTag[2] === 'USD' && exchangeRate && exchangeRate['USD']) {
|
||||
let usd = parseFloat(costTag[1]);
|
||||
sats = Math.ceil(exchangeRate['USD'].sats * usd * 1_000);
|
||||
}
|
||||
|
||||
let payload = {
|
||||
profile: recipient.pubkey,
|
||||
event: subEvent.id,
|
||||
amount: sats,
|
||||
relays: relays.map(r => r.url)
|
||||
};
|
||||
|
||||
if (subEvent.content.length > 0) {
|
||||
// @ts-ignore
|
||||
payload.comment = comment;
|
||||
}
|
||||
|
||||
const zapReq = nip57.makeZapRequest(payload);
|
||||
|
||||
try {
|
||||
const signedEvent = await signEvent(zapReq);
|
||||
|
||||
const event = encodeURIComponent(JSON.stringify(signedEvent));
|
||||
|
||||
const r2 = await (await fetch(`${callback}?amount=${sats}&nostr=${event}`)).json();
|
||||
const pr = r2.pr;
|
||||
|
||||
await enableWebLn();
|
||||
await sendPayment(pr);
|
||||
|
||||
return true;
|
||||
} catch (reason) {
|
||||
console.error('Failed to zap: ', reason);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const getZapEndpoint = async (user: PrimalUser): Promise<string | null> => {
|
||||
try {
|
||||
let lnurl: string = ''
|
||||
|
@ -4,6 +4,13 @@
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.readsFeed {
|
||||
position: relative;
|
||||
border: none;
|
||||
min-height: 100vh;
|
||||
border-top: 1px solid var(--devider);
|
||||
}
|
||||
|
||||
.paginate {
|
||||
color: var(--text-tertiary-2);
|
||||
position: absolute;
|
||||
|
@ -5,23 +5,77 @@
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--devider);
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.userName {
|
||||
color: var(--text-primary);
|
||||
.userInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.userName {
|
||||
display: flex;
|
||||
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;
|
||||
max-width: 80%;
|
||||
|
||||
.time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.client {
|
||||
&::before {
|
||||
content: '• ';
|
||||
}
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
.right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,14 +84,14 @@
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
margin-bottom: 48px;
|
||||
margin-bottom: 22px;
|
||||
margin-inline: 20px;
|
||||
|
||||
.title {
|
||||
color: var(--text-primary);
|
||||
font-size: 32px;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 40px;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
@ -53,10 +107,9 @@
|
||||
|
||||
.text {
|
||||
color: var(--text-primary);
|
||||
font-family: Lora;
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +155,8 @@
|
||||
padding: 6px 10px;
|
||||
border-radius: 12px;
|
||||
width: fit-content;
|
||||
margin: 4px;
|
||||
margin-block: 4px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -100,7 +100,7 @@
|
||||
}
|
||||
|
||||
.notificationTabContent {
|
||||
width: 602px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notificationTabIndicator {
|
||||
|
@ -83,7 +83,14 @@ const Notifications: Component = () => {
|
||||
const [relatedNotes, setRelatedNotes] = createStore<NotificationStore>({
|
||||
notes: [],
|
||||
users: [],
|
||||
page: { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {} },
|
||||
page: {
|
||||
messages: [],
|
||||
users: {},
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
topZaps: {},
|
||||
},
|
||||
reposts: {},
|
||||
})
|
||||
|
||||
@ -91,7 +98,15 @@ const Notifications: Component = () => {
|
||||
notes: [],
|
||||
users: {},
|
||||
userStats: {},
|
||||
page: { messages: [], users: {}, postStats: {}, notifications: [], mentions: {}, noteActions: {} },
|
||||
page: {
|
||||
messages: [],
|
||||
users: {},
|
||||
postStats: {},
|
||||
notifications: [],
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
topZaps: {},
|
||||
},
|
||||
reposts: {},
|
||||
notifications: [],
|
||||
})
|
||||
|
@ -19,13 +19,13 @@ import { useProfileContext } from '../contexts/ProfileContext';
|
||||
import { useAccountContext } from '../contexts/AccountContext';
|
||||
import Wormhole from '../components/Wormhole/Wormhole';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { sanitize } from '../lib/notes';
|
||||
import { sanitize, sendEvent } from '../lib/notes';
|
||||
import { shortDate } from '../lib/dates';
|
||||
|
||||
import styles from './Profile.module.scss';
|
||||
import StickySidebar from '../components/StickySidebar/StickySidebar';
|
||||
import ProfileSidebar from '../components/ProfileSidebar/ProfileSidebar';
|
||||
import { MenuItem, VanityProfiles, ZapOption } from '../types/primal';
|
||||
import { MenuItem, PrimalUser, VanityProfiles, ZapOption } from '../types/primal';
|
||||
import PageTitle from '../components/PageTitle/PageTitle';
|
||||
import FollowButton from '../components/FollowButton/FollowButton';
|
||||
import Search from '../components/Search/Search';
|
||||
@ -44,6 +44,13 @@ import NoteImage from '../components/NoteImage/NoteImage';
|
||||
import ProfileQrCodeModal from '../components/ProfileQrCodeModal/ProfileQrCodeModal';
|
||||
import { CustomZapInfo, useAppContext } from '../contexts/AppContext';
|
||||
import ProfileAbout from '../components/ProfileAbout/ProfileAbout';
|
||||
import ButtonPrimary from '../components/Buttons/ButtonPrimary';
|
||||
import { Tier, TierCost } from '../components/SubscribeToAuthorModal/SubscribeToAuthorModal';
|
||||
import { Kind } from '../constants';
|
||||
import { getAuthorSubscriptionTiers } from '../lib/feed';
|
||||
import { zapSubscription } from '../lib/zap';
|
||||
import { updateStore, store } from '../services/StoreService';
|
||||
import { subsTo } from '../sockets';
|
||||
|
||||
const Profile: Component = () => {
|
||||
|
||||
@ -66,6 +73,8 @@ const Profile: Component = () => {
|
||||
const [confirmMuteUser, setConfirmMuteUser] = createSignal(false);
|
||||
const [openQr, setOpenQr] = createSignal(false);
|
||||
|
||||
const [hasTiers, setHasTiers] = createSignal(false);
|
||||
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
gallery: '#central_header',
|
||||
children: 'a.profile_image',
|
||||
@ -118,11 +127,13 @@ const Profile: Component = () => {
|
||||
const setProfile = (hex: string | undefined) => {
|
||||
profile?.actions.setProfileKey(hex);
|
||||
|
||||
profile?.actions.clearArticles();
|
||||
profile?.actions.clearNotes();
|
||||
profile?.actions.clearReplies();
|
||||
profile?.actions.clearContacts();
|
||||
profile?.actions.clearZaps();
|
||||
profile?.actions.clearFilterReason();
|
||||
setHasTiers(() => false);
|
||||
}
|
||||
|
||||
let keyIsDone = false
|
||||
@ -503,6 +514,94 @@ const Profile: Component = () => {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
if (profile?.userProfile) {
|
||||
getTiers(profile.userProfile);
|
||||
}
|
||||
});
|
||||
|
||||
const getTiers = (author: PrimalUser) => {
|
||||
if (!author) return;
|
||||
|
||||
const subId = `article_tiers_${APP_ID}`;
|
||||
|
||||
const unsub = subsTo(subId, {
|
||||
onEvent: (_, content) => {
|
||||
if (content.kind === Kind.TierList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (content.kind === Kind.Tier) {
|
||||
setHasTiers(() => true);
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
onEose: () => {
|
||||
unsub();
|
||||
},
|
||||
})
|
||||
|
||||
getAuthorSubscriptionTiers(author.pubkey, subId);
|
||||
}
|
||||
|
||||
const doSubscription = async (tier: Tier, cost: TierCost, exchangeRate?: Record<string, Record<string, number>>) => {
|
||||
const a = profile?.userProfile;
|
||||
|
||||
if (!a || !account || !cost) return;
|
||||
|
||||
const subEvent = {
|
||||
kind: Kind.Subscribe,
|
||||
content: '',
|
||||
created_at: Math.floor((new Date()).getTime() / 1_000),
|
||||
tags: [
|
||||
['p', a.pubkey],
|
||||
['e', tier.id],
|
||||
['amount', cost.amount, cost.unit, cost.cadence],
|
||||
['event', JSON.stringify(tier.event)],
|
||||
// Copy any zap splits
|
||||
...(tier.event.tags?.filter(t => t[0] === 'zap') || []),
|
||||
],
|
||||
}
|
||||
|
||||
const { success, note } = await sendEvent(subEvent, account.relays, account.relaySettings);
|
||||
|
||||
if (success && note) {
|
||||
const isZapped = await zapSubscription(note, a, account.publicKey, account.relays, exchangeRate);
|
||||
|
||||
if (!isZapped) {
|
||||
unsubscribe(note.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = async (eventId: string) => {
|
||||
const a = profile?.userProfile;;
|
||||
|
||||
if (!a || !account) return;
|
||||
|
||||
const unsubEvent = {
|
||||
kind: Kind.Unsubscribe,
|
||||
content: '',
|
||||
created_at: Math.floor((new Date()).getTime() / 1_000),
|
||||
|
||||
tags: [
|
||||
['p', a.pubkey],
|
||||
['e', eventId],
|
||||
],
|
||||
};
|
||||
|
||||
await sendEvent(unsubEvent, account.relays, account.relaySettings);
|
||||
|
||||
}
|
||||
|
||||
|
||||
const openSubscribe = () => {
|
||||
app?.actions.openAuthorSubscribeModal(profile?.userProfile, doSubscription);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={
|
||||
@ -599,6 +698,14 @@ const Profile: Component = () => {
|
||||
<FollowButton person={profile?.userProfile} large={true} />
|
||||
</Show>
|
||||
|
||||
<Show when={hasTiers()}>
|
||||
<ButtonPrimary
|
||||
onClick={openSubscribe}
|
||||
>
|
||||
subscribe
|
||||
</ButtonPrimary>
|
||||
</Show>
|
||||
|
||||
<Show when={isCurrentUser()}>
|
||||
<div class={styles.editProfileButton}>
|
||||
<ButtonSecondary
|
||||
|
@ -35,6 +35,9 @@ import { useAppContext } from '../contexts/AppContext';
|
||||
import { useReadsContext } from '../contexts/ReadsContext';
|
||||
import ArticlePreview from '../components/ArticlePreview/ArticlePreview';
|
||||
import PageCaption from '../components/PageCaption/PageCaption';
|
||||
import ReadsSidebar from '../components/HomeSidebar/ReadsSidebar';
|
||||
import ReedSelect from '../components/FeedSelect/ReedSelect';
|
||||
import ReadsHeader from '../components/HomeHeader/ReadsHeader';
|
||||
|
||||
|
||||
const Home: Component = () => {
|
||||
@ -94,7 +97,7 @@ const Home: Component = () => {
|
||||
}
|
||||
|
||||
if (newPostAuthors.length < 3) {
|
||||
const users = context?.future.notes.map(note => note.author) || [];
|
||||
const users = context?.future.notes.map(note => note.user) || [];
|
||||
|
||||
const uniqueUsers = users.reduce<PrimalUser[]>((acc, user) => {
|
||||
const isDuplicate = acc.find(u => u && u.pubkey === user.pubkey);
|
||||
@ -125,6 +128,10 @@ const Home: Component = () => {
|
||||
setNewPostAuthors(() => []);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
context?.actions.doSidebarSearch('')
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={styles.homeContent}>
|
||||
<PageTitle title={intl.formatMessage(branding)} />
|
||||
@ -134,39 +141,48 @@ const Home: Component = () => {
|
||||
<Search />
|
||||
</Wormhole>
|
||||
|
||||
<PageCaption title={intl.formatMessage(reads.pageTitle)} />
|
||||
<PageCaption title={intl.formatMessage(reads.pageTitle)}>
|
||||
<ReadsHeader
|
||||
hasNewPosts={() => {}}
|
||||
loadNewContent={() => {}}
|
||||
newPostCount={() => {}}
|
||||
newPostAuthors={[]}
|
||||
/>
|
||||
</PageCaption>
|
||||
|
||||
<StickySidebar>
|
||||
<HomeSidebar />
|
||||
<ReadsSidebar />
|
||||
</StickySidebar>
|
||||
|
||||
<Show
|
||||
when={context?.notes && context.notes.length > 0}
|
||||
>
|
||||
<div class={styles.feed}>
|
||||
<For each={context?.notes} >
|
||||
{note => <ArticlePreview article={note} />}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.readsFeed}>
|
||||
<Show
|
||||
when={context?.notes && context.notes.length > 0}
|
||||
>
|
||||
<div class={styles.feed}>
|
||||
<For each={context?.notes} >
|
||||
{note => <ArticlePreview article={note} />}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Switch>
|
||||
<Match
|
||||
when={!isPageLoading() && context?.notes && context?.notes.length === 0}
|
||||
>
|
||||
<div class={styles.noContent}>
|
||||
<Loader />
|
||||
</div>
|
||||
</Match>
|
||||
<Match
|
||||
when={isPageLoading()}
|
||||
>
|
||||
<div class={styles.noContent}>
|
||||
<Loader />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Paginator loadNextPage={context?.actions.fetchNextPage}/>
|
||||
<Switch>
|
||||
<Match
|
||||
when={!isPageLoading() && context?.notes && context?.notes.length === 0}
|
||||
>
|
||||
<div class={styles.noContent}>
|
||||
<Loader />
|
||||
</div>
|
||||
</Match>
|
||||
<Match
|
||||
when={isPageLoading()}
|
||||
>
|
||||
<div class={styles.noContent}>
|
||||
<Loader />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Paginator loadNextPage={context?.actions.fetchNextPage}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -62,6 +62,7 @@
|
||||
--icon-network-popular: url('./assets/icons/network_popular.svg');
|
||||
--icon-network-trending: url('./assets/icons/network_trending.svg');
|
||||
|
||||
--reads-placeholder-image: url('./assets/images/reads_image_dark.png');
|
||||
|
||||
select {
|
||||
background-color: var(--background-site);
|
||||
@ -133,6 +134,7 @@
|
||||
--icon-network-popular: url('./assets/icons/network_popular.svg');
|
||||
--icon-network-trending: url('./assets/icons/network_trending.svg');
|
||||
|
||||
--reads-placeholder-image: url('./assets/images/reads_image_dark.png');
|
||||
|
||||
select {
|
||||
background-color: var(--background-site);
|
||||
@ -203,6 +205,7 @@
|
||||
--icon-network-popular: url('./assets/icons/network_popular.svg');
|
||||
--icon-network-trending: url('./assets/icons/network_trending.svg');
|
||||
|
||||
--reads-placeholder-image: url('./assets/images/reads_image_light.png');
|
||||
|
||||
select {
|
||||
background-color: var(--background-site);
|
||||
@ -274,6 +277,7 @@
|
||||
--icon-network-popular: url('./assets/icons/network_popular.svg');
|
||||
--icon-network-trending: url('./assets/icons/network_trending.svg');
|
||||
|
||||
--reads-placeholder-image: url('./assets/images/reads_image_light.png');
|
||||
|
||||
select {
|
||||
background-color: var(--background-site);
|
||||
|
@ -366,8 +366,11 @@ export const convertToNotes: ConvertToNotes = (page, topZaps) => {
|
||||
replyTo: replyTo && replyTo[1],
|
||||
tags: msg.tags,
|
||||
id: msg.id,
|
||||
noteId: nip19.noteEncode(msg.id),
|
||||
pubkey: msg.pubkey,
|
||||
topZaps: [ ...tz ],
|
||||
content: sanitize(msg.content),
|
||||
relayHints: page.relayHints,
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -466,10 +469,11 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
|
||||
image: '',
|
||||
tags: [],
|
||||
published: msg.created_at || 0,
|
||||
content: msg.content,
|
||||
author: convertToUser(user),
|
||||
content: sanitize(msg.content),
|
||||
user: user ? convertToUser(user) : emptyUser(msg.pubkey),
|
||||
topZaps: [...tz],
|
||||
naddr: nip19.naddrEncode({ identifier, pubkey, kind }),
|
||||
noteId: nip19.naddrEncode({ identifier, pubkey, kind }),
|
||||
msg,
|
||||
mentionedNotes,
|
||||
mentionedUsers,
|
||||
@ -483,6 +487,7 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
|
||||
score: stat?.score || 0,
|
||||
score24h: stat?.score24h || 0,
|
||||
satszapped: stat?.satszapped || 0,
|
||||
relayHints: page.relayHints,
|
||||
};
|
||||
|
||||
msg.tags.forEach(tag => {
|
||||
@ -502,6 +507,9 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
|
||||
case 'published':
|
||||
article.published = parseInt(tag[1]);
|
||||
break;
|
||||
case 'client':
|
||||
article.client = tag[1];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -1193,6 +1193,11 @@ export const profile = {
|
||||
defaultMessage: 'Total',
|
||||
description: 'Label for total sats profile stat',
|
||||
},
|
||||
articles: {
|
||||
id: 'profile.stats.articles',
|
||||
defaultMessage: 'Reads',
|
||||
description: 'Label for reads profile stat',
|
||||
},
|
||||
notes: {
|
||||
id: 'profile.stats.notes',
|
||||
defaultMessage: 'Notes',
|
||||
|
45
src/types/primal.d.ts
vendored
45
src/types/primal.d.ts
vendored
@ -242,6 +242,37 @@ export type NostrWordCount = {
|
||||
tags?: string[][],
|
||||
};
|
||||
|
||||
export type NostrTierList = {
|
||||
kind: Kind.TierList,
|
||||
content: string,
|
||||
created_at?: number,
|
||||
tags?: string[][],
|
||||
};
|
||||
|
||||
export type NostrTier = {
|
||||
kind: Kind.Tier,
|
||||
content: string,
|
||||
created_at?: number,
|
||||
id: string,
|
||||
tags?: string[][],
|
||||
};
|
||||
|
||||
export type NostrSubscribe = {
|
||||
kind: Kind.Subscribe,
|
||||
content: string,
|
||||
created_at?: number,
|
||||
id: string,
|
||||
tags?: string[][],
|
||||
};
|
||||
|
||||
export type NostrUnsubscribe = {
|
||||
kind: Kind.Unsubscribe,
|
||||
content: string,
|
||||
created_at?: number,
|
||||
id: string,
|
||||
tags?: string[][],
|
||||
};
|
||||
|
||||
export type NostrEventContent =
|
||||
NostrNoteContent |
|
||||
NostrUserContent |
|
||||
@ -274,6 +305,10 @@ export type NostrEventContent =
|
||||
NostrRelayHint |
|
||||
NostrZapInfo |
|
||||
NostrQuoteStatsInfo |
|
||||
NostrTierList |
|
||||
NostrTier |
|
||||
NostrSubscribe |
|
||||
NostrUnsubscribe |
|
||||
NostrWordCount;
|
||||
|
||||
export type NostrEvent = [
|
||||
@ -498,8 +533,11 @@ export type PrimalNote = {
|
||||
replyTo?: string,
|
||||
id: string,
|
||||
pubkey: string,
|
||||
noteId: string,
|
||||
tags: string[][],
|
||||
topZaps: TopZap[],
|
||||
content: string,
|
||||
relayHints?: Record<string, string>,
|
||||
};
|
||||
|
||||
|
||||
@ -510,7 +548,7 @@ export type PrimalArticle = {
|
||||
tags: string[],
|
||||
published: number,
|
||||
content: string,
|
||||
author: PrimalUser,
|
||||
user: PrimalUser,
|
||||
topZaps: TopZap[],
|
||||
repost?: PrimalRepost,
|
||||
mentionedNotes?: Record<string, PrimalNote>,
|
||||
@ -519,6 +557,7 @@ export type PrimalArticle = {
|
||||
id: string,
|
||||
pubkey: string,
|
||||
naddr: string,
|
||||
noteId: string,
|
||||
msg: NostrNoteContent,
|
||||
wordCount: number,
|
||||
noteActions: NoteActions,
|
||||
@ -530,6 +569,8 @@ export type PrimalArticle = {
|
||||
score: number,
|
||||
score24h: number,
|
||||
satszapped: number,
|
||||
client?: string,
|
||||
relayHints?: Record<string, string>,
|
||||
};
|
||||
|
||||
export type PrimalFeed = {
|
||||
@ -800,3 +841,5 @@ export type PageRange = {
|
||||
until: number,
|
||||
order_by: string,
|
||||
};
|
||||
|
||||
export type EventCoordinate = { pubkey: string, identifier: string, kind: number };
|
||||
|
Loading…
Reference in New Issue
Block a user