More subs stuff

This commit is contained in:
Bojan Mojsilovic 2024-06-06 17:15:00 +02:00
parent fcb3926e67
commit bd09ed7668
11 changed files with 292 additions and 19 deletions

View File

@ -4,7 +4,7 @@
gap: 8px;
border-radius: 8px;
background: var(--background-header-input);
width: 268px;
width: 300px;
padding: 16px;
.userInfo {
@ -49,8 +49,9 @@
.actions {
display: flex;
justify-content: space-between;
justify-content: flex-end;
align-items: center;
gap: 6px;
>button {
color: var(--text-primary);

View File

@ -3,12 +3,15 @@ import { batch, Component, createEffect, createSignal, For, JSXElement, onMount,
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';
@ -22,6 +25,7 @@ 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';
@ -55,8 +59,34 @@ const AuthoreSubscribe: Component<{
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());
app?.actions.openAuthorSubscribeModal(author(), doSubscription);
};
return (
@ -82,7 +112,10 @@ const AuthoreSubscribe: Component<{
{author()?.about || ''}
</div>
<div class={styles.actions}>
<ButtonSecondary onClick={() => navigate(`/p/${author()?.npub}`)}>
<ButtonSecondary
light={true}
onClick={() => navigate(`/p/${author()?.npub}`)}
>
view profile
</ButtonSecondary>

View File

@ -77,6 +77,8 @@
.readsSidebar {
margin-left: -8px;
.section {
margin-bottom: 28px;

View File

@ -113,6 +113,9 @@ const ReadsSidebar: Component< { id?: string } > = (props) => {
// const author = 'a8eb6e07bf408713b0979f337a3cd978f622e0d41709f3b74b48fff43dbfcd2b';
// setFeautredAuthor(() => author);
// const author = '88cc134b1a65f54ef48acc1df3665063d3ea45f04eab8af4646e561c5ae99079';
// setFeautredAuthor(() => author);
setFeautredAuthor(() => authors[Math.floor(Math.random() * authors.length)]);
},
onEose: () => {
@ -181,7 +184,9 @@ const ReadsSidebar: Component< { id?: string } > = (props) => {
<Loader />
}
>
<div class={styles.section}>
<AuthorSubscribe pubkey={featuredAuthor()} />
</div>
</Show>

View File

@ -196,7 +196,7 @@ const Layout: Component = () => {
<SubscribeToAuthorModal
author={app?.subscribeToAuthor}
onClose={app?.actions.closeAuthorSubscribeModal}
onSubscribe={() => {}}
onSubscribe={app?.subscribeToTier}
/>
</div>
</Show>

View File

@ -90,6 +90,7 @@
background: none;
margin: 0;
padding: 16px;
width: 400px;
&.selected {
border: 1px solid var(--accent);

View File

@ -44,7 +44,7 @@ const SubscribeToAuthorModal: Component<{
id?: string,
author: PrimalUser | undefined,
onClose: () => void,
onSubscribe: () => void,
onSubscribe: (tier: Tier) => void,
}> = (props) => {
const [store, updateStore] = createStore<TierStore>({
@ -107,6 +107,15 @@ const SubscribeToAuthorModal: Component<{
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}>
@ -176,7 +185,9 @@ const SubscribeToAuthorModal: Component<{
</div>
<div class={styles.payAction}>
<ButtonPrimary onClick={props.onSubscribe}>
<ButtonPrimary
onClick={() => store.selectedTier && props.onSubscribe(store.selectedTier)}
>
subscribe
</ButtonPrimary>
</div>

View File

@ -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 = {
@ -67,6 +70,7 @@ export type AppContextStore = {
confirmInfo: ConfirmInfo | undefined,
cashuMints: Map<string, CashuMint>,
subscribeToAuthor: PrimalUser | undefined,
subscribeToTier: (tier: Tier) => void,
actions: {
openReactionModal: (noteId: string, stats: ReactionStats) => void,
closeReactionModal: () => void,
@ -82,7 +86,7 @@ export type AppContextStore = {
openConfirmModal: (confirmInfo: ConfirmInfo) => void,
closeConfirmModal: () => void,
getCashuMint: (url: string) => CashuMint | undefined,
openAuthorSubscribeModal: (author: PrimalUser | undefined) => void,
openAuthorSubscribeModal: (author: PrimalUser | undefined, subscribeTo: (tier: Tier) => void) => void,
closeAuthorSubscribeModal: () => void,
},
}
@ -110,6 +114,7 @@ const initialData: Omit<AppContextStore, 'actions'> = {
confirmInfo: undefined,
cashuMints: new Map(),
subscribeToAuthor: undefined,
subscribeToTier: () => {},
};
export const AppContext = createContext<AppContextStore>();
@ -225,10 +230,11 @@ export const AppProvider = (props: { children: JSXElement }) => {
return store.cashuMints.get(formatted);
};
const openAuthorSubscribeModal = (author: PrimalUser | undefined, subscribeTo: (tier: Tier) => void) => {
if (!author) return;
const openAuthorSubscribeModal = (author: PrimalUser | undefined) => {
console.log('OPEN: ', author)
author && updateStore('subscribeToAuthor', () => ({ ...author }));
updateStore('subscribeToAuthor', () => ({ ...author }));
updateStore('subscribeToTier', () => subscribeTo);
};
const closeAuthorSubscribeModal = () => {

View File

@ -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 = ''

View File

@ -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}>

View File

@ -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