mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-28 16:00:50 +00:00
Compare commits
2 Commits
388c2e689d
...
bd09ed7668
Author | SHA1 | Date | |
---|---|---|---|
|
bd09ed7668 | ||
|
fcb3926e67 |
65
src/components/AuthorSubscribe/AuthorSubscribe.module.scss
Normal file
65
src/components/AuthorSubscribe/AuthorSubscribe.module.scss
Normal file
@ -0,0 +1,65 @@
|
||||
.authorSubscribeCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--background-header-input);
|
||||
width: 300px;
|
||||
padding: 16px;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userPitch {
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
>button {
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
}
|
||||
}
|
132
src/components/AuthorSubscribe/AuthorSubscribe.tsx
Normal file
132
src/components/AuthorSubscribe/AuthorSubscribe.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
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 { 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 { Tier } 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) => {
|
||||
const a = author();
|
||||
|
||||
if (!a || !account) return;
|
||||
|
||||
const subEvent = {
|
||||
kind: Kind.Subscribe,
|
||||
content: '',
|
||||
created_at: Math.floor((new Date()).getTime() / 1_000),
|
||||
tags: [
|
||||
['p', a.pubkey],
|
||||
['e', tier.id],
|
||||
['amount', tier.costs[0].amount, tier.costs[0].unit, tier.costs[0].duration],
|
||||
['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) {
|
||||
await zapSubscription(note, a, account.publicKey, account.relays);
|
||||
}
|
||||
}
|
||||
|
||||
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()} />
|
||||
<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);
|
@ -77,6 +77,8 @@
|
||||
|
||||
|
||||
.readsSidebar {
|
||||
margin-left: -8px;
|
||||
|
||||
.section {
|
||||
margin-bottom: 28px;
|
||||
|
||||
|
@ -18,11 +18,12 @@ import { useReadsContext } from '../../contexts/ReadsContext';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { APP_ID } from '../../App';
|
||||
import { subsTo } from '../../sockets';
|
||||
import { getArticleThread, getReadsTopics } from '../../lib/feed';
|
||||
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 = [
|
||||
{
|
||||
@ -73,11 +74,11 @@ const ReadsSidebar: Component< { id?: string } > = (props) => {
|
||||
|
||||
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);
|
||||
|
||||
@ -99,13 +100,41 @@ const ReadsSidebar: Component< { id?: string } > = (props) => {
|
||||
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);
|
||||
|
||||
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()
|
||||
getTopics();
|
||||
getFeaturedAuthor();
|
||||
}
|
||||
});
|
||||
|
||||
@ -146,7 +175,23 @@ const ReadsSidebar: Component< { id?: string } > = (props) => {
|
||||
<div id={props.id} class={styles.readsSidebar}>
|
||||
<Show when={account?.isKeyLookupDone}>
|
||||
<div class={styles.headingPicks}>
|
||||
Top Picks
|
||||
Featured Author
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={!isFetchingAuthors()}
|
||||
fallback={
|
||||
<Loader />
|
||||
}
|
||||
>
|
||||
<div class={styles.section}>
|
||||
<AuthorSubscribe pubkey={featuredAuthor()} />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
||||
<div class={styles.headingPicks}>
|
||||
Featured Reads
|
||||
</div>
|
||||
|
||||
<Show
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1,221 @@
|
||||
.subscribeToAuthor {
|
||||
position: fixed;
|
||||
min-width: 472px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
.tiers {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.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;
|
||||
|
||||
&.selected {
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.title: {
|
||||
color: var(--text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
200
src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx
Normal file
200
src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
// @ts-ignore
|
||||
import { decode } from 'light-bolt11-decoder';
|
||||
import { Component, createEffect, For, Show } from 'solid-js';
|
||||
import { createStore, reconcile } from 'solid-js/store';
|
||||
import { emptyInvoice, Kind } from '../../constants';
|
||||
import { date, dateFuture } from '../../lib/dates';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import { humanizeNumber } from '../../lib/stats';
|
||||
import { cashuInvoice } from '../../translations';
|
||||
import { LnbcInvoice, NostrTier, PrimalUser } from '../../types/primal';
|
||||
import ButtonPrimary from '../Buttons/ButtonPrimary';
|
||||
import Modal from '../Modal/Modal';
|
||||
import QrCode from '../QrCode/QrCode';
|
||||
import { getDecodedToken, Token } from "@cashu/cashu-ts";
|
||||
|
||||
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 } from '../../sockets';
|
||||
import { getAuthorSubscriptionTiers } from '../../lib/feed';
|
||||
import ButtonSecondary from '../Buttons/ButtonSecondary';
|
||||
|
||||
export type Tier = {
|
||||
title: string,
|
||||
content: string,
|
||||
id: string,
|
||||
perks: string[],
|
||||
costs: { amount: string, unit: string, duration: string}[],
|
||||
client: string,
|
||||
event: NostrTier,
|
||||
};
|
||||
|
||||
export type TierStore = {
|
||||
tiers: Tier[],
|
||||
selectedTier: Tier | undefined,
|
||||
}
|
||||
|
||||
export const payUnits = ['sats', 'msats', ''];
|
||||
|
||||
const SubscribeToAuthorModal: Component<{
|
||||
id?: string,
|
||||
author: PrimalUser | undefined,
|
||||
onClose: () => void,
|
||||
onSubscribe: (tier: Tier) => void,
|
||||
}> = (props) => {
|
||||
|
||||
const [store, updateStore] = createStore<TierStore>({
|
||||
tiers: [],
|
||||
selectedTier: undefined,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const author = props.author;
|
||||
|
||||
if (author) {
|
||||
getTiers(author);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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: t.tags?.filter((t: string[]) => t[0] === 'amount').map((t: string[]) => ({ amount: t[1], unit: t[2], duration: t[3]})) || [],
|
||||
client: (t.tags?.find((t: string[]) => t[0] === 'client') || [])[1] || t.content || '',
|
||||
event: t,
|
||||
}
|
||||
|
||||
tiers.push(tier)
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
onEose: () => {
|
||||
unsub();
|
||||
updateStore('tiers', () => [...tiers]);
|
||||
updateStore('selectedTier', () => ( tiers.length > 0 ? { ...tiers[0]} : undefined))
|
||||
},
|
||||
})
|
||||
|
||||
getAuthorSubscriptionTiers(author.pubkey, subId)
|
||||
}
|
||||
|
||||
const selectTier = (tier: Tier) => {
|
||||
updateStore('selectedTier', () => ({ ...tier }));
|
||||
}
|
||||
|
||||
const isSelectedTier = (tier: Tier) => tier.id === store.selectedTier?.id;
|
||||
|
||||
|
||||
// const costForTier = (tier: Tier) => {
|
||||
// const costs = tier.costs.filter(c => payUnits.includes(c.unit));
|
||||
|
||||
// costs.reduce((acc, c) => {
|
||||
// return
|
||||
// }, [])
|
||||
// }
|
||||
|
||||
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.body}>
|
||||
<div class={styles.tiers}>
|
||||
<For each={store.tiers}>
|
||||
{tier => (
|
||||
<button
|
||||
class={`${styles.tier} ${isSelectedTier(tier) ? styles.selected : ''}`}
|
||||
onClick={() => selectTier(tier)}
|
||||
>
|
||||
<div class={styles.title}>{tier.title}</div>
|
||||
<div class={styles.cost}>
|
||||
<div class={styles.amount}>
|
||||
{tier.costs[0].amount} {tier.costs[0].unit}
|
||||
</div>
|
||||
<div class={styles.duration}>
|
||||
{tier.costs[0].duration}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.footer}>
|
||||
<div class={styles.payAction}>
|
||||
<ButtonSecondary
|
||||
light={true}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
cancel
|
||||
</ButtonSecondary>
|
||||
</div>
|
||||
|
||||
<div class={styles.payAction}>
|
||||
<ButtonPrimary
|
||||
onClick={() => store.selectedTier && props.onSubscribe(store.selectedTier)}
|
||||
>
|
||||
subscribe
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</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 } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
|
||||
import { Kind } from "../constants";
|
||||
import { sendEvent } from "../lib/notes";
|
||||
|
||||
|
||||
export type ReactionStats = {
|
||||
@ -66,6 +69,8 @@ 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,
|
||||
@ -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) => 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>();
|
||||
@ -221,6 +230,17 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
return store.cashuMints.get(formatted);
|
||||
};
|
||||
|
||||
const openAuthorSubscribeModal = (author: PrimalUser | undefined, subscribeTo: (tier: Tier) => 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,
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Kind } from "./constants";
|
||||
import { getEvents, getUserArticleFeed } from "./lib/feed";
|
||||
import { decodeIdentifier } from "./lib/keys";
|
||||
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 { convertToArticles, convertToNotes } from "./stores/note";
|
||||
import { convertToUser } from "./stores/profile";
|
||||
import { account } from "./translations";
|
||||
import { EventCoordinate, FeedPage, NostrEventContent, NostrEventType, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalArticle, 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) => {
|
||||
@ -740,3 +742,43 @@ export const fetchUserArticles = (userPubkey: string | undefined, pubkey: string
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -365,3 +365,27 @@ export const getReadsTopics = (
|
||||
{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 }]},
|
||||
]));
|
||||
};
|
||||
|
@ -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";
|
||||
|
||||
@ -138,6 +140,62 @@ export const zapProfile = async (profile: PrimalUser, sender: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const zapSubscription = async (subEvent: NostrRelaySignedEvent, recipient: PrimalUser, sender: string | undefined, relays: Relay[]) => {
|
||||
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]);
|
||||
}
|
||||
|
||||
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 = ''
|
||||
|
@ -6,13 +6,13 @@ import { APP_ID } from "../App";
|
||||
import { Kind } from "../constants";
|
||||
import { useAccountContext } from "../contexts/AccountContext";
|
||||
import { decodeIdentifier } from "../lib/keys";
|
||||
import { getParametrizedEvent, setLinkPreviews } from "../lib/notes";
|
||||
import { subscribeTo } from "../sockets";
|
||||
import { getParametrizedEvent, sendEvent, setLinkPreviews } from "../lib/notes";
|
||||
import { subscribeTo, subsTo } from "../sockets";
|
||||
import { SolidMarkdown } from "solid-markdown";
|
||||
|
||||
import styles from './Longform.module.scss';
|
||||
import Loader from "../components/Loader/Loader";
|
||||
import { FeedPage, NostrEventContent, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalArticle, PrimalNote, PrimalUser, SendNoteResult, TopZap, ZapOption } from "../types/primal";
|
||||
import { FeedPage, NostrEventContent, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrTier, NostrUserContent, NoteActions, PrimalArticle, PrimalNote, PrimalUser, SendNoteResult, TopZap, ZapOption } from "../types/primal";
|
||||
import { getUserProfileInfo, getUserProfiles } from "../lib/profile";
|
||||
import { convertToUser, nip05Verification, userName } from "../stores/profile";
|
||||
import Avatar from "../components/Avatar/Avatar";
|
||||
@ -28,7 +28,7 @@ import NoteTopZaps from "../components/Note/NoteTopZaps";
|
||||
import { parseBolt11, uuidv4 } from "../utils";
|
||||
import Note, { NoteReactionsState } from "../components/Note/Note";
|
||||
import NoteFooter from "../components/Note/NoteFooter/NoteFooter";
|
||||
import { getArticleThread, getThread } from "../lib/feed";
|
||||
import { getArticleThread, getAuthorSubscriptionTiers, getThread } from "../lib/feed";
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||
import NoteImage from "../components/NoteImage/NoteImage";
|
||||
import { nip19 } from "nostr-tools";
|
||||
@ -48,6 +48,9 @@ import ArticleSidebar from "../components/HomeSidebar/ArticleSidebar";
|
||||
import ReplyToNote from "../components/ReplyToNote/ReplyToNote";
|
||||
import { sanitize } from "dompurify";
|
||||
import { fetchNotes } from "../handleNotes";
|
||||
import { Tier } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
|
||||
import ButtonPrimary from "../components/Buttons/ButtonPrimary";
|
||||
import { zapSubscription } from "../lib/zap";
|
||||
|
||||
export type LongFormData = {
|
||||
title: string,
|
||||
@ -69,6 +72,7 @@ export type LongformThreadStore = {
|
||||
users: PrimalUser[],
|
||||
isFetching: boolean,
|
||||
lastReply: PrimalNote | undefined,
|
||||
hasTiers: boolean,
|
||||
}
|
||||
|
||||
const emptyArticle = {
|
||||
@ -99,6 +103,7 @@ const emptyStore: LongformThreadStore = {
|
||||
users: [],
|
||||
isFetching: false,
|
||||
lastReply: undefined,
|
||||
hasTiers: false,
|
||||
}
|
||||
|
||||
const test = `
|
||||
@ -354,6 +359,68 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
fetchArticle();
|
||||
});
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
if (store.article?.user) {
|
||||
getTiers(store.article.user);
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
updateStore('hasTiers', () => true);
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
onEose: () => {
|
||||
unsub();
|
||||
},
|
||||
})
|
||||
|
||||
getAuthorSubscriptionTiers(author.pubkey, subId)
|
||||
}
|
||||
|
||||
const doSubscription = async (tier: Tier) => {
|
||||
const a = store.article?.user;
|
||||
|
||||
if (!a || !account) return;
|
||||
|
||||
const subEvent = {
|
||||
kind: Kind.Subscribe,
|
||||
content: '',
|
||||
created_at: Math.floor((new Date()).getTime() / 1_000),
|
||||
tags: [
|
||||
['p', a.pubkey],
|
||||
['e', tier.id],
|
||||
['amount', tier.costs[0].amount, tier.costs[0].unit, tier.costs[0].duration],
|
||||
['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) {
|
||||
await zapSubscription(note, a, account.publicKey, account.relays);
|
||||
}
|
||||
}
|
||||
|
||||
const openSubscribe = () => {
|
||||
app?.actions.openAuthorSubscribeModal(store.article?.user, doSubscription);
|
||||
};
|
||||
|
||||
const onConfirmZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
batch(() => {
|
||||
@ -823,6 +890,14 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
</A>
|
||||
|
||||
<Show when={store.hasTiers}>
|
||||
<ButtonPrimary
|
||||
onClick={openSubscribe}
|
||||
>
|
||||
subscribe
|
||||
</ButtonPrimary>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={styles.topBar}>
|
||||
|
@ -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 } 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',
|
||||
@ -124,6 +133,7 @@ const Profile: Component = () => {
|
||||
profile?.actions.clearContacts();
|
||||
profile?.actions.clearZaps();
|
||||
profile?.actions.clearFilterReason();
|
||||
setHasTiers(() => false);
|
||||
}
|
||||
|
||||
let keyIsDone = false
|
||||
@ -504,6 +514,69 @@ 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) => {
|
||||
const a = profile?.userProfile;
|
||||
|
||||
if (!a || !account) return;
|
||||
|
||||
const subEvent = {
|
||||
kind: Kind.Subscribe,
|
||||
content: '',
|
||||
created_at: Math.floor((new Date()).getTime() / 1_000),
|
||||
tags: [
|
||||
['p', a.pubkey],
|
||||
['e', tier.id],
|
||||
['amount', tier.costs[0].amount, tier.costs[0].unit, tier.costs[0].duration],
|
||||
['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) {
|
||||
await zapSubscription(note, a, account.publicKey, account.relays);
|
||||
}
|
||||
}
|
||||
|
||||
const openSubscribe = () => {
|
||||
app?.actions.openAuthorSubscribeModal(profile?.userProfile, doSubscription);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={
|
||||
@ -600,6 +673,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
src/types/primal.d.ts
vendored
35
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 = [
|
||||
|
Loading…
Reference in New Issue
Block a user