Compare commits

..

14 Commits

Author SHA1 Message Date
Dominik
9e10e7a9f3
Merge 6c73249ba7 into 55f6c0a4d7 2024-05-28 12:26:37 -05:00
Bojan Mojsilovic
55f6c0a4d7 New home header 2024-05-28 19:12:26 +02:00
Bojan Mojsilovic
b4857db4bf New feed note layout 2024-05-28 18:57:00 +02:00
Bojan Mojsilovic
5883af3336 Widen content 2024-05-28 17:48:38 +02:00
Bojan Mojsilovic
83059fffa8 New nav menu styling 2024-05-28 17:25:20 +02:00
Bojan Mojsilovic
e7cac58cdf Fix test page 2024-05-28 16:41:51 +02:00
Bojan Mojsilovic
d88364d705 Enable reactions 2024-05-28 16:41:51 +02:00
Bojan Mojsilovic
1580ee6e1b Reads page 2024-05-28 16:41:51 +02:00
Bojan Mojsilovic
6d7a828c2d WIP 2024-05-28 16:41:51 +02:00
Bojan Mojsilovic
2a7476f979 Refactor thread route 2024-05-28 16:32:39 +02:00
Bojan Mojsilovic
3bbb03c401 Adjust styling for lf notes 2024-05-24 16:15:30 +02:00
Bojan Mojsilovic
7e635a29ae First markdown render try 2024-05-24 16:15:30 +02:00
Bojan Mojsilovic
428e5050c9 Markdown test 2024-05-24 16:15:30 +02:00
Bojan Mojsilovic
9eab38fd08 Basic longform render 2024-05-24 16:15:30 +02:00
62 changed files with 447 additions and 4227 deletions

View File

@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,5 +1,4 @@
.article, .articleShort {
position: relative;
.article {
display: flex;
flex-direction: column;
text-decoration: none;
@ -44,10 +43,6 @@
font-size: 14px;
font-weight: 400;
line-height: 14px;
&::before {
content: '';
}
}
}
@ -66,28 +61,17 @@
.content {
.title {
color: var(--text-primary);
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;
font-size: 20px;
font-weight: 700;
line-height: 28px;
}
.summary {
color: var(--brand-text);
color: var(--text-primary);
font-family: Lora;
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;
}
}
@ -104,44 +88,23 @@
padding: 6px 10px;
border-radius: 12px;
width: fit-content;
margin-block: 4px;
margin-inline: 3px;
margin: 4px;
}
.estimate {
display: inline-block;
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
font-weight: 400;
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 {
max-width: 162px;
max-height: 238px;
object-fit: scale-down;
}
.placeholderImage {
width: 164px;
height: 118px;
background-image: var(--reads-placeholder-image);
background-size: contain;
object-fit: scale-down;
}
}
@ -152,65 +115,3 @@
}
}
.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;
}
}
}
}

View File

@ -12,7 +12,6 @@ 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';
@ -54,7 +53,6 @@ const ArticlePreview: Component<{
let latestTopZap: string = '';
let latestTopZapFeed: string = '';
let articleContextMenu: HTMLDivElement | undefined;
const onConfirmZap = (zapOption: ZapOption) => {
app?.actions.closeCustomZapModal();
@ -183,48 +181,19 @@ 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.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>
<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>
</div>
<div class={styles.time}>
{shortDate(props.article.published)}
</div>
</div>
<div class={styles.body}>
<div class={styles.text}>
<div class={styles.content}>
@ -236,30 +205,20 @@ const ArticlePreview: Component<{
</div>
</div>
<div class={styles.tags}>
<div class={styles.estimate}>
{Math.ceil(props.article.wordCount / 238)} minute read
</div>
<For each={props.article.tags.slice(0, 3)}>
<For each={props.article.tags}>
{tag => (
<div class={styles.tag}>
{tag}
</div>
)}
</For>
<Show when={props.article.tags.length > 3}>
<div class={styles.tag}>
+ {props.article.tags.length - 3}
</div>
</Show>
<div class={styles.estimate}>
{Math.ceil(props.article.wordCount / 238)} minute read
</div>
</div>
</div>
<div class={styles.image}>
<Show
when={props.article.image}
fallback={<div class={styles.placeholderImage}></div>}
>
<img src={props.article.image} />
</Show>
<img src={props.article.image} />
</div>
</div>

View File

@ -1,65 +0,0 @@
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);

View File

@ -1,66 +0,0 @@
.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%;
}
}
}

View File

@ -1,153 +0,0 @@
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);

View File

@ -164,21 +164,6 @@
}
}
.mlAvatar {
@include avatar;
width: 60px;
height: 60px;
.missingBack {
width: 60px;
height: 60px;
}
.iconBackground {
@include iconBackground;
}
}
.largeAvatar {
@include avatar;
width: 72px;
@ -305,13 +290,6 @@
font-size: 16px;
}
.mlMissing {
@include missing;
width: 60px;
height: 60px;
font-size: 16px;
}
.largeMissing {
@include missing;
width: 68px;

View File

@ -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" | "ml" | "lg" | "xl" | "xxl",
size?: "micro" | "xxs" | "xss" | "xs" | "vvs" | "vs" | "sm" | "md" | "lg" | "xl" | "xxl",
user?: PrimalUser,
highlightBorder?: boolean,
id?: string,
@ -34,7 +34,6 @@ const Avatar: Component<{
vs: styles.vsAvatar,
sm: styles.smallAvatar,
md: styles.midAvatar,
ml: styles.mlAvatar,
lg: styles.largeAvatar,
xl: styles.extraLargeAvatar,
xxl: styles.xxlAvatar,
@ -49,7 +48,6 @@ const Avatar: Component<{
vs: styles.vsMissing,
sm: styles.smallMissing,
md: styles.midMissing,
ml: styles.mlMissing,
lg: styles.largeMissing,
xl: styles.extraLargeMissing,
xxl: styles.xxlMissing,

View File

@ -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 | undefined, large?: boolean }> = (props) => {
const BookmarkArticle: Component<{ note: PrimalArticle, large?: boolean }> = (props) => {
const account = useAccountContext();
const app = useAppContext();
const intl = useIntl();
@ -24,13 +24,8 @@ const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: bool
const [isBookmarked, setIsBookmarked] = createSignal(false);
const [bookmarkInProgress, setBookmarkInProgress] = createSignal(false);
createEffect(() => {
const note = props.note;
if (note) {
setIsBookmarked(() => account?.bookmarks.includes(note.id) || false);
}
setIsBookmarked(() => account?.bookmarks.includes(props.note.id) || false);
})
const updateBookmarks = async (bookmarkTags: string[][]) => {
@ -52,7 +47,7 @@ const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: bool
};
const addBookmark = async (bookmarkTags: string[][]) => {
if (account && props.note && !bookmarkTags.find(b => b[0] === 'e' && b[1] === props.note?.id)) {
if (account && !bookmarkTags.find(b => b[0] === 'e' && b[1] === props.note.id)) {
const bookmarksToAdd = [...bookmarkTags, ['e', props.note.id]];
if (bookmarksToAdd.length < 2) {
@ -78,8 +73,8 @@ const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: bool
}
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}`);

View File

@ -37,14 +37,3 @@
}
}
}
.right {
display: flex;
justify-content: flex-end;
padding-left: 6px !important;
padding-right: 0px !important;
&.rightL {
width: 22px !important;
}
}

View File

@ -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, right?: boolean }> = (props) => {
const BookmarkNote: Component<{ note: PrimalNote, large?: boolean }> = (props) => {
const account = useAccountContext();
const app = useAppContext();
const intl = useIntl();
@ -136,7 +136,6 @@ const BookmarkNote: Component<{ note: PrimalNote, large?: boolean, right?: boole
return (
<div class={styles.bookmark}>
<ButtonGhost
class={`${props.right ? styles.right : ''} ${props.large ? styles.rightL : ''}`}
onClick={(e: MouseEvent) => {
e.preventDefault();

View File

@ -118,7 +118,6 @@ const EmbeddedNote: Component<{
shorten={true}
isEmbeded={true}
width={noteContent?.getBoundingClientRect().width}
margins={2}
/>
</div>
</>

View File

@ -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, big?: boolean}> = (props) => {
const FeedSelect: Component<{ isPhone?: boolean, id?: string}> = (props) => {
const account = useAccountContext();
const home = useHomeContext();
@ -123,7 +123,6 @@ const FeedSelect: Component<{ isPhone?: boolean, id?: string, big?: boolean}> =
value={selectedValue()}
isSelected={isSelected}
isPhone={props.isPhone}
big={props.big}
/>
);
}

View File

@ -1,129 +0,0 @@
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);

View File

@ -123,13 +123,6 @@
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);
@ -162,7 +155,7 @@
outline: none;
padding-block: 21px;
padding-inline: 20px;
padding-inline: 12px;
border: none;
border-radius: 0;
justify-content: flex-start;

View File

@ -119,7 +119,7 @@ const HomeHeader: Component< {
<Show
when={settings?.availableFeeds && settings?.availableFeeds.length > 0 && home?.selectedFeed}
>
<FeedSelect big={true} />
<FeedSelect />
</Show>
<Show

View File

@ -1,37 +0,0 @@
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);

View File

@ -1,96 +0,0 @@
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);

View File

@ -56,99 +56,3 @@
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;
}
}

View File

@ -1,240 +0,0 @@
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);

View File

@ -21,7 +21,6 @@ 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);
@ -192,12 +191,6 @@ 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>

View File

@ -115,11 +115,10 @@
display: flex;
justify-content: center;
align-items: center;
min-width: 18px;
width: 18px;
height: 18px;
margin-left: 2px;
margin-top: -8px;
font-family: 'Roboto Condensed';
font-weight: 500;

View File

@ -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, PrimalArticle, PrimalNote, PrimalUser, SendNoteResult } from "../../../types/primal";
import { EmojiOption, FeedPage, NostrMentionContent, NostrNoteContent, NostrStatsContent, NostrUserContent, 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 | PrimalArticle,
replyToNote?: PrimalNote,
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?.noteId);
const draftUserRefs = readNoteDraftUserRefs(account?.publicKey, props.replyToNote?.noteId);
const draft = readNoteDraft(account?.publicKey, props.replyToNote?.post.noteId);
const draftUserRefs = readNoteDraftUserRefs(account?.publicKey, props.replyToNote?.post.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?.noteId);
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.noteId);
saveNoteDraft(account?.publicKey, message(), props.replyToNote?.post.noteId);
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.post.noteId);
});
const onEscape = (e: KeyboardEvent) => {
@ -667,8 +667,8 @@ const EditBox: Component<{
return;
}
saveNoteDraft(account?.publicKey, '', props.replyToNote?.noteId);
saveNoteDraftUserRefs(account?.publicKey, {}, props.replyToNote?.noteId);
saveNoteDraft(account?.publicKey, '', props.replyToNote?.post.noteId);
saveNoteDraftUserRefs(account?.publicKey, {}, props.replyToNote?.post.noteId);
clearEditor();
};
@ -680,8 +680,8 @@ const EditBox: Component<{
};
const persistNote = (note: string) => {
saveNoteDraft(account?.publicKey, note, props.replyToNote?.noteId);
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.noteId);
saveNoteDraft(account?.publicKey, note, props.replyToNote?.post.noteId);
saveNoteDraftUserRefs(account?.publicKey, userRefs, props.replyToNote?.post.noteId);
clearEditor();
};
@ -721,10 +721,10 @@ const EditBox: Component<{
const rep = props.replyToNote;
if (rep) {
let rootTag = rep.msg.tags.find(t => t[0] === 'e' && t[3] === 'root');
let rootTag = rep.post.tags.find(t => t[0] === 'e' && t[3] === 'root');
const rHints = (rep.relayHints && rep.relayHints[rep.id]) ?
rep.relayHints[rep.id] :
const rHints = (rep.post.relayHints && rep.post.relayHints[rep.post.id]) ?
rep.post.relayHints[rep.post.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.id, rHints, 'reply']);
tags.push(['e', rep.post.id, rHints, 'reply']);
}
// Otherwise, add the note as the root tag for this reply
else {
tags.push([
'e',
rep.id,
rep.post.id,
rHints,
'root',
]);
}
// Copy all `p` tags from the note we are repling to
const repPeople = rep.msg.tags.filter(t => t[0] === 'p');
const repPeople = rep.post.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.pubkey)) {
tags.push(['p', rep.pubkey]);
if (!tags.find(t => t[0] === 'p' && t[1] === rep.post.pubkey)) {
tags.push(['p', rep.post.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?.noteId)
saveNoteDraft(account.publicKey, '', rep?.post.noteId)
clearEditor();
}
unsub();
@ -1046,7 +1046,7 @@ const EditBox: Component<{
// // setNoteRefs((refs) => ({
// // ...refs,
// // [newNote.noteId]: newNote
// // [newNote.post.noteId]: newNote
// // }));
subUserRef(hex);
@ -1107,10 +1107,10 @@ const EditBox: Component<{
setNoteRefs((refs) => ({
...refs,
[newNote.noteId]: newNote
[newNote.post.noteId]: newNote
}));
subNoteRef(newNote.noteId);
subNoteRef(newNote.post.noteId);
unsub();
return;

View File

@ -3,7 +3,7 @@
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 20px;
padding: 12px;
margin: 0px;
text-decoration: none;
@ -361,16 +361,12 @@
background-color: var(--background-card);
display: flex;
flex-direction: column;
padding-inline: 0px;
padding-inline: 12px;
padding-top: 0;
padding-bottom: 12px;
border-radius: 0;
border: none;
.header {
padding-inline: 12px;
}
.content {
grid-area: content;
display: flex;
@ -390,7 +386,6 @@
line-height: 24px;
width: 100%;
margin-bottom: 12px;
padding-inline: 12px;
a:hover {
text-decoration: underline;
@ -411,7 +406,6 @@
.time {
padding-block: 20px;
padding-inline: 12px;
border-bottom: 1px solid var(--devider);
margin-bottom: 16px;
color: var(--text-tertiary);
@ -451,13 +445,6 @@
}
}
.topZaps {
padding-inline: 12px;
}
.footer {
padding-inline: 12px;
}
}
}

View File

@ -276,10 +276,7 @@ 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}>
@ -288,7 +285,6 @@ const Note: Component<{
state={reactionsState}
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
size="short"
/>
</div>
</div>
@ -305,9 +301,7 @@ const Note: Component<{
>
<div class={styles.border}></div>
<div class={styles.header}>
<NoteHeader note={props.note} primary={true} />
</div>
<NoteHeader note={props.note} primary={true} />
<div class={styles.upRightFloater}>
<NoteContextTrigger
@ -319,20 +313,14 @@ const Note: Component<{
<div class={styles.content}>
<div class={styles.message}>
<ParsedNote
note={props.note}
width={Math.min(640, window.innerWidth)}
margins={12}
/>
<ParsedNote note={props.note} width={Math.min(640, window.innerWidth)} />
</div>
<div class={styles.topZaps}>
<NoteTopZaps
topZaps={reactionsState.topZaps}
zapCount={reactionsState.zapCount}
action={() => openReactionModal('zaps')}
/>
</div>
<NoteTopZaps
topZaps={reactionsState.topZaps}
zapCount={reactionsState.zapCount}
action={() => openReactionModal('zaps')}
/>
<div
class={styles.time}
@ -349,17 +337,15 @@ const Note: Component<{
</button>
</div>
<div class={styles.footer}>
<NoteFooter
note={props.note}
state={reactionsState}
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
size="wide"
large={true}
onZapAnim={addTopZap}
/>
</div>
<NoteFooter
note={props.note}
state={reactionsState}
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
wide={true}
large={true}
onZapAnim={addTopZap}
/>
</div>
</div>
</Match>
@ -404,7 +390,6 @@ const Note: Component<{
note={props.note}
shorten={props.shorten}
width={Math.min(640, window.innerWidth - 72)}
margins={20}
/>
</div>
@ -421,6 +406,7 @@ const Note: Component<{
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
onZapAnim={addTopZapFeed}
wide={true}
/>
</A>
</Match>
@ -471,8 +457,7 @@ const Note: Component<{
<ParsedNote
note={props.note}
shorten={props.shorten}
width={Math.min(566, window.innerWidth - 72)}
margins={1}
width={Math.min(528, window.innerWidth - 72)}
/>
</div>
@ -483,16 +468,13 @@ const Note: Component<{
topZapLimit={4}
/>
<div class={styles.footer}>
<NoteFooter
note={props.note}
state={reactionsState}
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
onZapAnim={addTopZapFeed}
size="short"
/>
</div>
<NoteFooter
note={props.note}
state={reactionsState}
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
onZapAnim={addTopZapFeed}
/>
</div>
</div>
</A>
@ -533,7 +515,6 @@ const Note: Component<{
note={props.note}
shorten={props.shorten}
width={Math.min(528, window.innerWidth - 72)}
margins={12}
noLightbox={true}
altEmbeds={true}
/>

View File

@ -60,12 +60,12 @@ const NoteContextMenu: Component<{
const doMuteUser = () => {
account?.actions.addToMuteList(note()?.pubkey);
account?.actions.addToMuteList(note()?.post.pubkey);
props.onClose();
};
const doUnmuteUser = () => {
account?.actions.removeFromMuteList(note()?.pubkey);
account?.actions.removeFromMuteList(note()?.post.pubkey);
props.onClose();
};
@ -77,21 +77,21 @@ const NoteContextMenu: Component<{
const copyNoteLink = () => {
if (!props.data) return;
navigator.clipboard.writeText(`${window.location.origin}/e/${note().noteId}`);
navigator.clipboard.writeText(`${window.location.origin}/e/${note().post.noteId}`);
props.onClose()
toaster?.sendSuccess(intl.formatMessage(tToast.notePrimalLinkCoppied));
};
const copyNoteText = () => {
if (!props.data) return;
navigator.clipboard.writeText(`${note().content}`);
navigator.clipboard.writeText(`${note().post.content}`);
props.onClose()
toaster?.sendSuccess(intl.formatMessage(tToast.notePrimalTextCoppied));
};
const copyNoteId = () => {
if (!props.data) return;
navigator.clipboard.writeText(`${note().noteId}`);
navigator.clipboard.writeText(`${note().post.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().id}`)?.contains(e.target as Node)
!document?.getElementById(`note_context_${note().post.id}`)?.contains(e.target as Node)
) {
props.onClose()
}
@ -222,7 +222,7 @@ const NoteContextMenu: Component<{
];
};
const noteContext = () => account?.publicKey !== note()?.pubkey ?
const noteContext = () => account?.publicKey !== note()?.post.pubkey ?
[ ...noteContextForEveryone, ...noteContextForOtherPeople()] :
noteContextForEveryone;
@ -251,7 +251,7 @@ const NoteContextMenu: Component<{
/>
<PrimalMenu
id={`note_context_${note()?.id}`}
id={`note_context_${note()?.post.id}`}
items={noteContext()}
hidden={!props.open}
position="note_footer"

View File

@ -28,7 +28,7 @@ export const lottieDuration = () => zapMD.op * 1_000 / zapMD.fr;
const ArticleFooter: Component<{
note: PrimalArticle,
size?: 'wide' | 'normal' | 'short',
wide?: boolean,
id?: string,
state: NoteReactionsState,
updateState: SetStoreFunction<NoteReactionsState>,
@ -49,8 +49,6 @@ const ArticleFooter: Component<{
let footerDiv: HTMLDivElement | undefined;
let repostMenu: HTMLDivElement | undefined;
const size = () => props.size ?? 'normal';
const repostMenuItems: MenuItem[] = [
{
action: () => doRepost(),
@ -183,7 +181,7 @@ const ArticleFooter: Component<{
return;
}
if (!canUserReceiveZaps(props.note.user)) {
if (!canUserReceiveZaps(props.note.author)) {
toast?.sendWarning(
intl.formatMessage(t.zapUnavailable),
);
@ -208,7 +206,7 @@ const ArticleFooter: Component<{
return;
}
if (account.relays.length === 0 || !canUserReceiveZaps(props.note.user)) {
if (account.relays.length === 0 || !canUserReceiveZaps(props.note.author)) {
return;
}
@ -225,17 +223,12 @@ const ArticleFooter: Component<{
return;
}
let newLeft = 33;
let newTop = -6;
let newLeft = props.wide ? 15 : 13;
let newTop = props.wide ? -6 : -6;
if (size() === 'wide' && props.large) {
newLeft = 14;
newTop = -10;
}
if (size() === 'short') {
newLeft = 14;
newTop = -6;
if (props.large) {
newLeft = 2;
newTop = -9;
}
medZapAnimation.style.left = `${newLeft}px`;
@ -326,12 +319,7 @@ const ArticleFooter: Component<{
}
return (
<div
id={props.id}
class={`${styles.footer} ${styles[size()]}`}
ref={footerDiv}
onClick={(e) => {e.preventDefault();}}
>
<div id={props.id} class={`${styles.footer} ${props.wide ? styles.wide : ''}`} ref={footerDiv} onClick={(e) => {e.preventDefault();}}>
<Show when={props.state.showZapAnim}>
<ZapAnimation

View File

@ -55,38 +55,12 @@
.footer {
display: grid;
grid-template-columns: 145px 145px 145px 145px auto;
grid-template-columns: 125px 125px 125px 125px auto;
position: relative;
width: 100%;
.bookmarkFoot {
display: flex;
justify-content: flex-end;
max-width: 20px;
}
&.wide {
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%;
grid-template-columns: 137px 137px 137px 135px auto;
}
.context {
@ -198,6 +172,10 @@
}
}
.bookmarkFoot {
display: flex;
justify-content: flex-end;
}
}
.largeZapLottie {

View File

@ -27,7 +27,7 @@ export const lottieDuration = () => zapMD.op * 1_000 / zapMD.fr;
const NoteFooter: Component<{
note: PrimalNote,
size?: 'wide' | 'normal' | 'short',
wide?: boolean,
id?: string,
state: NoteReactionsState,
updateState: SetStoreFunction<NoteReactionsState>,
@ -48,8 +48,6 @@ const NoteFooter: Component<{
let footerDiv: HTMLDivElement | undefined;
let repostMenu: HTMLDivElement | undefined;
const size = () => props.size ?? 'normal';
const repostMenuItems: MenuItem[] = [
{
action: () => doRepost(),
@ -63,7 +61,6 @@ const NoteFooter: Component<{
},
];
const onClickOutside = (e: MouseEvent) => {
if (
!document?.getElementById(`repost_menu_${props.note.post.id}`)?.contains(e.target as Node)
@ -225,17 +222,12 @@ const NoteFooter: Component<{
return;
}
let newLeft = 33;
let newTop = -6;
let newLeft = props.wide ? 15 : 13;
let newTop = props.wide ? -6 : -6;
if (size() === 'wide' && props.large) {
newLeft = 14;
newTop = -10;
}
if (size() === 'short') {
newLeft = 14;
newTop = -6;
if (props.large) {
newLeft = 2;
newTop = -9;
}
medZapAnimation.style.left = `${newLeft}px`;
@ -326,12 +318,7 @@ const NoteFooter: Component<{
}
return (
<div
id={props.id}
class={`${styles.footer} ${styles[size()]}`}
ref={footerDiv}
onClick={(e) => e.preventDefault() }
>
<div id={props.id} class={`${styles.footer} ${props.wide ? styles.wide : ''}`} ref={footerDiv} onClick={(e) => {e.preventDefault();}}>
<Show when={props.state.showZapAnim}>
<ZapAnimation
@ -408,7 +395,6 @@ const NoteFooter: Component<{
<BookmarkNote
note={props.note}
large={props.large}
right={true}
/>
</div>

View File

@ -4,14 +4,13 @@ import { useThreadContext } from "../../contexts/ThreadContext";
import Avatar from "../Avatar/Avatar";
import { TransitionGroup } from 'solid-transition-group';
import styles from "./Note.module.scss";
import { PrimalUser, TopZap } from "../../types/primal";
import { TopZap } from "../../types/primal";
const NoteTopZaps: Component<{
topZaps: TopZap[],
zapCount: number,
action: () => void,
id?: string,
users?: PrimalUser[]
}> = (props) => {
const threadContext = useThreadContext();
@ -43,7 +42,7 @@ const NoteTopZaps: Component<{
}
const zapSender = (zap: TopZap) => {
return (props.users || threadContext?.users || []).find(u => u.pubkey === zap.pubkey);
return threadContext?.users.find(u => u.pubkey === zap.pubkey);
};
return (

View File

@ -147,7 +147,6 @@ const ParsedNote: Component<{
shorten?: boolean,
isEmbeded?: boolean,
width?: number,
margins?: number,
noLightbox?: boolean,
altEmbeds?: boolean,
}> = (props) => {
@ -620,10 +619,9 @@ const ParsedNote: Component<{
let w: number | undefined = undefined;
if (mVideo) {
const margins = props.margins || 20
const ratio = mVideo.w / mVideo.h;
h = ((noteWidth() - 2*margins) / ratio);
w = h > 680 ? 680 * ratio : noteWidth() - 2*margins;
h = (noteWidth() / ratio);
w = h > 680 ? 680 * ratio : noteWidth();
h = h > 680 ? 680 : h;
}

View File

@ -9,229 +9,33 @@
.editor {
margin-block: 8px;
p {
* {
color: var(--text-primary);
font-size: 16px;
}
p, li {
font-family: Lora;
font-size: 15px;
font-style: normal;
font-weight: 400;
line-height: 24px;
margin-top: 0;
margin-bottom: 20px;
}
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: 24px;
padding-left: 12px;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 20px;
border-left: 4px solid var(--text-tertiary);
line-height: 22px;
}
a {
color: var(--accent-links);
font-size: 16px;
font-family: Lora;
font-size: 15px;
font-style: normal;
font-weight: 400;
line-height: 24px;
line-height: 22px;
}
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;
}
}
}
}
}

View File

@ -27,7 +27,7 @@ import { APP_ID } from '../../App';
import { getUserProfileInfo } from '../../lib/profile';
import { useAccountContext } from '../../contexts/AccountContext';
import { Kind } from '../../constants';
import { PrimalArticle, PrimalNote, PrimalUser } from '../../types/primal';
import { PrimalNote, PrimalUser } from '../../types/primal';
import { convertToUser, userName } from '../../stores/profile';
import { A } from '@solidjs/router';
import { createStore } from 'solid-js/store';
@ -35,14 +35,11 @@ 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();
@ -52,24 +49,6 @@ 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);
@ -118,20 +97,6 @@ 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]+)/;
@ -143,7 +108,7 @@ const account = useAccountContext();
const [nostr, id] = match;
if (id.startsWith('npub')) {
if (id.startsWith('npub1')) {
fetchUserInfo(id);
return (
@ -156,7 +121,7 @@ const account = useAccountContext();
);
}
if (id.startsWith('note')) {
if (id.startsWith('note1')) {
fetchNoteInfo(id);
return (
<Show
@ -234,16 +199,13 @@ const account = useAccountContext();
<div ref={ref} class={styles.editor} style="display: none;" />
<div id={id()} class={styles.editor}>
<div 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>

View File

@ -13,7 +13,6 @@ 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";
@ -134,9 +133,6 @@ 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;
@ -165,18 +161,6 @@ 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}>
@ -247,62 +231,6 @@ 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

View File

@ -9,32 +9,6 @@
}
.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;
@ -44,7 +18,11 @@
mask: url(../../assets/icons/caret.svg) no-repeat 0 / 14px;
}
.triggerBig {
.selectionBox {
position: relative;
}
.trigger {
background-color: var(--background-site);
margin: 0;
padding: 0;

View File

@ -18,7 +18,6 @@ const SelectionBox: Component<{
options: SelectionOption[],
onChange: (option: any) => void,
initialValue?: string | SelectionOption,
big?: boolean,
value?: string | SelectionOption,
id?: string,
}> = (props) => {
@ -56,12 +55,12 @@ const SelectionBox: Component<{
onChange={props.onChange}
gutter={8}
>
<Select.Trigger class={props.big ? styles.triggerBig : styles.trigger}>
<Select.Trigger class={styles.trigger}>
<Select.Value<SelectionOption>>
{state => state.selectedOption()?.label || ''}
</Select.Value>
<Select.Icon>
<div class={props.big ? styles.selectionIconBig : styles.selectionIcon}></div>
<div class={styles.selectionIcon}></div>
</Select.Icon>
</Select.Trigger>
<Select.Content>

View File

@ -1,290 +0,0 @@
.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;
}

View File

@ -1,383 +0,0 @@
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);

View File

@ -102,19 +102,16 @@ export enum Kind {
ChannelHideMessage = 43,
ChannelMuteUser = 44,
Subscribe = 7_001,
Unsubscribe = 7_002,
LongForm = 30_023,
Zap = 9_735,
MuteList = 10_000,
RelayList = 10_002,
Bookmarks = 10_003,
TierList = 17_000,
CategorizedPeople = 30_000,
LongForm = 30_023,
Settings = 30_078,
Tier = 37_001,
ACK = 10_000_098,
NoteStats = 10_000_100,
@ -146,7 +143,6 @@ export enum Kind {
RelayHint=10_000_141,
NoteQuoteStats=10_000_143,
WordCount=10_000_144,
FeaturedAuthors=10_000_148,
WALLET_OPERATION = 10_000_300,
}

View File

@ -9,9 +9,6 @@ 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 = {
@ -32,7 +29,7 @@ export type CustomZapInfo = {
};
export type NoteContextMenuInfo = {
note: PrimalNote | PrimalArticle,
note: PrimalNote,
position: DOMRect | undefined,
openCustomZap?: () => void,
openReactions?: () => void,
@ -69,15 +66,13 @@ 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 | PrimalArticle, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
openContextMenu: (note: PrimalNote, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
closeContextMenu: () => void,
openLnbcModal: (lnbc: string, onPay: () => void) => void,
closeLnbcModal: () => void,
@ -86,8 +81,6 @@ 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,
},
}
@ -113,8 +106,6 @@ const initialData: Omit<AppContextStore, 'actions'> = {
showConfirmModal: false,
confirmInfo: undefined,
cashuMints: new Map(),
subscribeToAuthor: undefined,
subscribeToTier: () => {},
};
export const AppContext = createContext<AppContextStore>();
@ -164,7 +155,7 @@ export const AppProvider = (props: { children: JSXElement }) => {
};
const openContextMenu = (
note: PrimalNote | PrimalArticle,
note: PrimalNote,
position: DOMRect | undefined,
openCustomZap: () => void,
openReactions: () => void,
@ -230,17 +221,6 @@ 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(() => {
@ -293,8 +273,6 @@ export const AppProvider = (props: { children: JSXElement }) => {
openCashuModal,
closeCashuModal,
getCashuMint,
openAuthorSubscribeModal,
closeAuthorSubscribeModal,
}
});

View File

@ -29,7 +29,6 @@ import {
NostrUserContent,
NostrUserZaps,
NoteActions,
PrimalArticle,
PrimalNote,
PrimalUser,
PrimalZap,
@ -53,7 +52,6 @@ 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,
@ -65,7 +63,6 @@ export type UserStats = {
total_zap_count: number,
total_satszapped: number,
relay_count: number,
long_form_note_count?: number,
};
export type ProfileContextStore = {
@ -74,7 +71,6 @@ export type ProfileContextStore = {
userStats: UserStats,
fetchedUserStats: boolean,
knownProfiles: VanityProfiles,
articles: PrimalArticle[],
notes: PrimalNote[],
replies: PrimalNote[],
zaps: PrimalZap[],
@ -95,7 +91,6 @@ 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[] },
@ -123,9 +118,6 @@ 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,
@ -164,7 +156,6 @@ export const initialData = {
userStats: { ...emptyStats },
fetchedUserStats: false,
knownProfiles: { names: {} },
articles: [],
notes: [],
replies: [],
isFetching: false,
@ -179,7 +170,6 @@ export const initialData = {
zappers: {},
zapListOffset: 0,
lastNote: undefined,
lastArticle: undefined,
lastReply: undefined,
lastZap: undefined,
following: [],
@ -475,20 +465,6 @@ 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;
@ -523,11 +499,6 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
}));
};
const clearArticles = () => {
updateStore('articles', () => []);
updateStore('lastArticle', () => undefined);
};
const clearReplies = () => {
updateStore('repliesPage', () => ({ messages: [], users: {}, postStats: {}, noteActions: {} }));
updateStore('replies', () => []);
@ -574,28 +545,6 @@ 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];
@ -1408,9 +1357,6 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
clearNotes,
fetchNotes,
fetchNextPage,
fetchArticles,
fetchNextArticlesPage,
clearArticles,
updatePage,
savePage,
saveReplies,

View File

@ -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, minKnownProfiles } from "../constants";
import { Kind } 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 { getRecomendedArticleIds, getScoredUsers, searchContent } from "../lib/search";
import { 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,7 +41,6 @@ type ReadsContextStore = {
lastNote: PrimalArticle | undefined,
reposts: Record<string, string> | undefined,
mentionedNotes: Record<string, NostrNoteContent>,
recomendedReads: string[],
future: {
notes: PrimalArticle[],
page: FeedPage,
@ -120,7 +119,6 @@ const initialHomeData = {
isFetching: false,
query: undefined,
},
recomendedReads: [],
};
export const ReadsContext = createContext<ReadsContextStore>();
@ -203,13 +201,13 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
};
const doSidebarSearch = (query: string) => {
const subid = `reads_recomended_${APP_ID}`;
const subid = `reads_sidebar_${APP_ID}`;
updateStore('sidebar', 'isFetching', () => true);
updateStore('sidebar', 'notes', () => []);
updateStore('sidebar', 'page', { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {}, topZaps: {} });
updateStore('sidebar', 'page', { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {} });
getRecomendedArticleIds(subid);
getScoredUsers(account?.publicKey, query, 10, subid);
}
const clearFuture = () => {
@ -309,8 +307,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
};
const fetchNotes = (topic: string, subId: string, until = 0, includeReplies?: boolean) => {
const t = topic === 'none' ? '' : topic;//account?.publicKey || '532d830dffe09c13e75e8b145c825718fc12b0003f61d61e9077721c7fff93cb';
const [scope, timeframe] = t.split(';');
const [scope, timeframe] = topic.split(';');
updateStore('isFetching', true);
updateStore('page', () => ({ messages: [], users: {}, postStats: {} }));
@ -332,7 +329,7 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
return;
}
getArticlesFeed(account?.publicKey, t, `reads_feed_${subId}`, until, 20);
getArticlesFeed(account?.publicKey, topic, `reads_feed_${subId}`, until, 20);
};
const clearNotes = () => {
@ -394,7 +391,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();
@ -492,6 +489,7 @@ 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',
@ -526,6 +524,7 @@ 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 } })
@ -647,27 +646,6 @@ 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);
@ -793,9 +771,9 @@ export const ReadsProvider = (props: { children: ContextChildren }) => {
});
createEffect(() => {
if (account?.isKeyLookupDone && account.publicKey) {
selectFeed({ hex: account.publicKey, name: 'My Reads'});
if (account?.isKeyLookupDone && settings?.defaultFeed) {
const storedFeed = fetchStoredFeed(account.publicKey);
selectFeed(storedFeed || settings?.defaultFeed);
}
});

View File

@ -1,21 +1,20 @@
import { nip19 } from "nostr-tools";
import { Kind } from "./constants";
import { getEvents, getUserArticleFeed } from "./lib/feed";
import { decodeIdentifier, hexToNpub } from "./lib/keys";
import { getParametrizedEvents, setLinkPreviews } from "./lib/notes";
import { getUserProfileInfo } from "./lib/profile";
import { getEvents } from "./lib/feed";
import { setLinkPreviews } from "./lib/notes";
import { updateStore, store } from "./services/StoreService";
import { subscribeTo } from "./sockets";
import { convertToArticles, convertToNotes } from "./stores/note";
import { convertToUser } from "./stores/profile";
import { convertToNotes } from "./stores/note";
import { account } from "./translations";
import { EventCoordinate, FeedPage, NostrEventContent, NostrEventType, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalArticle, PrimalNote, PrimalUser, TopZap } from "./types/primal";
import { FeedPage, NostrEventContent, NostrEventType, NostrMentionContent, NostrNoteActionsContent, NostrNoteContent, NostrStatsContent, NostrUserContent, NoteActions, PrimalNote, 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: [],
@ -183,601 +182,3 @@ 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;
}
};
});
}

View File

@ -62,10 +62,10 @@
--warning-color: #FA3C3C;
--success-color: #66E205;
--left-col-w: 182px;
--left-col-w: 188px;
--center-col-w: 640px;
--right-col-w: 348px;
--full-site-w: 1172px;
--full-site-w: 1177px;
--header-height: 84px;
background-color: var(--background-site);
@ -144,9 +144,7 @@ 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/images/reads_image_dark.png)
url(./assets/images/reads_image_light.png);
url(./assets/icons/nav/long_selected.svg);
}
.reply_icon {

View File

@ -27,12 +27,11 @@ export const getFeed = (user_pubkey: string | undefined, pubkey: string | undef
return;
}
const time = until === 0 ? Math.ceil((new Date()).getTime()/1_000 ): until;
const start = until === 0 ? 'since' : 'until';
let payload = { limit, until: time, pubkey };
let payload = { limit, [start]: until, pubkey };
if (user_pubkey) {
// @ts-ignore dynamic property
payload.user_pubkey = user_pubkey;
}
if (include_replies) {
@ -48,21 +47,15 @@ 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 };
if (pubkey && pubkey?.length > 0) {
// @ts-ignore
payload.pubkey = pubkey;
}
let payload = { limit, [start]: until, pubkey };
if (user_pubkey) {
// @ts-ignore
payload.user_pubkey = user_pubkey;
}
@ -141,34 +134,6 @@ 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,
@ -355,37 +320,3 @@ 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 }]},
]));
};

View File

@ -36,39 +36,3 @@ 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');
}
}

View File

@ -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 { EventCoordinate, MediaSize, NostrRelays, NostrRelaySignedEvent, PrimalArticle, PrimalNote, SendNoteResult } from "../types/primal";
import { 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.pubkey],
['p', note.author.pubkey],
],
created_at: Math.floor((new Date()).getTime() / 1000),
};
@ -615,12 +615,3 @@ 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 }]},
]));
};

View File

@ -75,11 +75,3 @@ 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']},
]));
};

View File

@ -1,9 +1,7 @@
import { bech32 } from "@scure/base";
// @ts-ignore Bad types in nostr-tools
import { nip57, Relay, utils } from "nostr-tools";
import { Tier } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
import { Kind } from "../constants";
import { NostrRelaySignedEvent, PrimalArticle, PrimalNote, PrimalUser } from "../types/primal";
import { PrimalArticle, PrimalNote, PrimalUser } from "../types/primal";
import { logError } from "./logger";
import { enableWebLn, sendPayment, signEvent } from "./nostrAPI";
@ -21,8 +19,8 @@ export const zapNote = async (note: PrimalNote, sender: string | undefined, amou
const sats = Math.round(amount * 1000);
let payload = {
profile: note.pubkey,
event: note.id,
profile: note.post.pubkey,
event: note.msg.id,
amount: sats,
relays: relays.map(r => r.url)
};
@ -57,7 +55,7 @@ export const zapArticle = async (note: PrimalArticle, sender: string | undefined
return false;
}
const callback = await getZapEndpoint(note.user);
const callback = await getZapEndpoint(note.author);
if (!callback) {
return false;
@ -140,67 +138,6 @@ 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 = ''

View File

@ -4,13 +4,6 @@
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;

View File

@ -5,77 +5,23 @@
padding: 20px;
border-bottom: 1px solid var(--devider);
a {
text-decoration: none;
}
.author {
display: flex;
justify-content: flex-start;
align-items: center;
gap: 6px;
.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);
.userName {
color: var(--text-primary);
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;
}
}
.right {
display: flex;
justify-content: flex-end;
align-items: center;
.time {
color: var(--text-tertiary);
font-size: 14px;
font-weight: 700;
}
}
@ -84,14 +30,14 @@
flex-direction: column;
gap: 20px;
position: relative;
margin-bottom: 22px;
margin-bottom: 48px;
margin-inline: 20px;
.title {
color: var(--text-primary);
font-size: 36px;
font-size: 32px;
font-weight: 700;
line-height: 44px;
line-height: 40px;
}
.summary {
@ -107,9 +53,10 @@
.text {
color: var(--text-primary);
font-size: 16px;
font-family: Lora;
font-size: 15px;
font-weight: 400;
line-height: 24px;
line-height: 22px;
}
}
@ -155,8 +102,7 @@
padding: 6px 10px;
border-radius: 12px;
width: fit-content;
margin-block: 4px;
margin-right: 6px;
margin: 4px;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -100,7 +100,7 @@
}
.notificationTabContent {
width: 100%;
width: 602px;
}
.notificationTabIndicator {

View File

@ -83,14 +83,7 @@ const Notifications: Component = () => {
const [relatedNotes, setRelatedNotes] = createStore<NotificationStore>({
notes: [],
users: [],
page: {
messages: [],
users: {},
postStats: {},
mentions: {},
noteActions: {},
topZaps: {},
},
page: { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {} },
reposts: {},
})
@ -98,15 +91,7 @@ const Notifications: Component = () => {
notes: [],
users: {},
userStats: {},
page: {
messages: [],
users: {},
postStats: {},
notifications: [],
mentions: {},
noteActions: {},
topZaps: {},
},
page: { messages: [], users: {}, postStats: {}, notifications: [], mentions: {}, noteActions: {} },
reposts: {},
notifications: [],
})

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, sendEvent } from '../lib/notes';
import { sanitize } 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, PrimalUser, VanityProfiles, ZapOption } from '../types/primal';
import { MenuItem, VanityProfiles, ZapOption } from '../types/primal';
import PageTitle from '../components/PageTitle/PageTitle';
import FollowButton from '../components/FollowButton/FollowButton';
import Search from '../components/Search/Search';
@ -44,13 +44,6 @@ 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 = () => {
@ -73,8 +66,6 @@ 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',
@ -127,13 +118,11 @@ 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
@ -514,94 +503,6 @@ 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={
@ -698,14 +599,6 @@ 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

View File

@ -35,9 +35,6 @@ 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 = () => {
@ -97,7 +94,7 @@ const Home: Component = () => {
}
if (newPostAuthors.length < 3) {
const users = context?.future.notes.map(note => note.user) || [];
const users = context?.future.notes.map(note => note.author) || [];
const uniqueUsers = users.reduce<PrimalUser[]>((acc, user) => {
const isDuplicate = acc.find(u => u && u.pubkey === user.pubkey);
@ -128,10 +125,6 @@ const Home: Component = () => {
setNewPostAuthors(() => []);
}
onMount(() => {
context?.actions.doSidebarSearch('')
})
return (
<div class={styles.homeContent}>
<PageTitle title={intl.formatMessage(branding)} />
@ -141,48 +134,39 @@ const Home: Component = () => {
<Search />
</Wormhole>
<PageCaption title={intl.formatMessage(reads.pageTitle)}>
<ReadsHeader
hasNewPosts={() => {}}
loadNewContent={() => {}}
newPostCount={() => {}}
newPostAuthors={[]}
/>
</PageCaption>
<PageCaption title={intl.formatMessage(reads.pageTitle)} />
<StickySidebar>
<ReadsSidebar />
<HomeSidebar />
</StickySidebar>
<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>
<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}/>
</div>
<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>
)
}

View File

@ -62,7 +62,6 @@
--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);
@ -134,7 +133,6 @@
--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);
@ -205,7 +203,6 @@
--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);
@ -277,7 +274,6 @@
--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);

View File

@ -366,11 +366,8 @@ 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,
};
});
}
@ -469,11 +466,10 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
image: '',
tags: [],
published: msg.created_at || 0,
content: sanitize(msg.content),
user: user ? convertToUser(user) : emptyUser(msg.pubkey),
content: msg.content,
author: convertToUser(user),
topZaps: [...tz],
naddr: nip19.naddrEncode({ identifier, pubkey, kind }),
noteId: nip19.naddrEncode({ identifier, pubkey, kind }),
msg,
mentionedNotes,
mentionedUsers,
@ -487,7 +483,6 @@ 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 => {
@ -507,9 +502,6 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
case 'published':
article.published = parseInt(tag[1]);
break;
case 'client':
article.client = tag[1];
break;
default:
break;
}

View File

@ -1193,11 +1193,6 @@ 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
View File

@ -242,37 +242,6 @@ 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 |
@ -305,10 +274,6 @@ export type NostrEventContent =
NostrRelayHint |
NostrZapInfo |
NostrQuoteStatsInfo |
NostrTierList |
NostrTier |
NostrSubscribe |
NostrUnsubscribe |
NostrWordCount;
export type NostrEvent = [
@ -533,11 +498,8 @@ export type PrimalNote = {
replyTo?: string,
id: string,
pubkey: string,
noteId: string,
tags: string[][],
topZaps: TopZap[],
content: string,
relayHints?: Record<string, string>,
};
@ -548,7 +510,7 @@ export type PrimalArticle = {
tags: string[],
published: number,
content: string,
user: PrimalUser,
author: PrimalUser,
topZaps: TopZap[],
repost?: PrimalRepost,
mentionedNotes?: Record<string, PrimalNote>,
@ -557,7 +519,6 @@ export type PrimalArticle = {
id: string,
pubkey: string,
naddr: string,
noteId: string,
msg: NostrNoteContent,
wordCount: number,
noteActions: NoteActions,
@ -569,8 +530,6 @@ export type PrimalArticle = {
score: number,
score24h: number,
satszapped: number,
client?: string,
relayHints?: Record<string, string>,
};
export type PrimalFeed = {
@ -841,5 +800,3 @@ export type PageRange = {
until: number,
order_by: string,
};
export type EventCoordinate = { pubkey: string, identifier: string, kind: number };