mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Basic subscribe flow
This commit is contained in:
parent
388c2e689d
commit
fcb3926e67
64
src/components/AuthorSubscribe/AuthorSubscribe.module.scss
Normal file
64
src/components/AuthorSubscribe/AuthorSubscribe.module.scss
Normal file
@ -0,0 +1,64 @@
|
||||
.authorSubscribeCard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--background-header-input);
|
||||
width: 268px;
|
||||
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: space-between;
|
||||
align-items: center;
|
||||
|
||||
>button {
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
}
|
||||
}
|
99
src/components/AuthorSubscribe/AuthorSubscribe.tsx
Normal file
99
src/components/AuthorSubscribe/AuthorSubscribe.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
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 { 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 { 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 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 openSubscribe = () => {
|
||||
app?.actions.openAuthorSubscribeModal(author());
|
||||
};
|
||||
|
||||
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 onClick={() => navigate(`/p/${author()?.npub}`)}>
|
||||
view profile
|
||||
</ButtonSecondary>
|
||||
|
||||
<ButtonPrimary onClick={openSubscribe}>
|
||||
subscribe
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(AuthoreSubscribe);
|
@ -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,38 @@ 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);
|
||||
|
||||
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 +172,21 @@ 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 />
|
||||
}
|
||||
>
|
||||
<AuthorSubscribe pubkey={featuredAuthor()} />
|
||||
</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={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -0,0 +1,220 @@
|
||||
.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;
|
||||
|
||||
&.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;
|
||||
}
|
189
src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx
Normal file
189
src/components/SubscribeToAuthorModal/SubscribeToAuthorModal.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
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: () => 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;
|
||||
|
||||
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={props.onSubscribe}>
|
||||
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,
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ export type AppContextStore = {
|
||||
showConfirmModal: boolean,
|
||||
confirmInfo: ConfirmInfo | undefined,
|
||||
cashuMints: Map<string, CashuMint>,
|
||||
subscribeToAuthor: PrimalUser | undefined,
|
||||
actions: {
|
||||
openReactionModal: (noteId: string, stats: ReactionStats) => void,
|
||||
closeReactionModal: () => void,
|
||||
@ -81,6 +82,8 @@ export type AppContextStore = {
|
||||
openConfirmModal: (confirmInfo: ConfirmInfo) => void,
|
||||
closeConfirmModal: () => void,
|
||||
getCashuMint: (url: string) => CashuMint | undefined,
|
||||
openAuthorSubscribeModal: (author: PrimalUser | undefined) => void,
|
||||
closeAuthorSubscribeModal: () => void,
|
||||
},
|
||||
}
|
||||
|
||||
@ -106,6 +109,7 @@ const initialData: Omit<AppContextStore, 'actions'> = {
|
||||
showConfirmModal: false,
|
||||
confirmInfo: undefined,
|
||||
cashuMints: new Map(),
|
||||
subscribeToAuthor: undefined,
|
||||
};
|
||||
|
||||
export const AppContext = createContext<AppContextStore>();
|
||||
@ -221,6 +225,16 @@ export const AppProvider = (props: { children: JSXElement }) => {
|
||||
return store.cashuMints.get(formatted);
|
||||
};
|
||||
|
||||
|
||||
const openAuthorSubscribeModal = (author: PrimalUser | undefined) => {
|
||||
console.log('OPEN: ', author)
|
||||
author && updateStore('subscribeToAuthor', () => ({ ...author }));
|
||||
};
|
||||
|
||||
const closeAuthorSubscribeModal = () => {
|
||||
updateStore('subscribeToAuthor', () => undefined);
|
||||
};
|
||||
|
||||
// EFFECTS --------------------------------------
|
||||
|
||||
onMount(() => {
|
||||
@ -273,6 +287,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 }]},
|
||||
]));
|
||||
};
|
||||
|
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