mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-28 16:00:50 +00:00
Compare commits
5 Commits
8a7140e109
...
6d23df743d
Author | SHA1 | Date | |
---|---|---|---|
|
6d23df743d | ||
|
70872beb5e | ||
|
dd1ba52f38 | ||
|
01ebf6a6d6 | ||
|
09db5fbfb1 |
6
package-lock.json
generated
6
package-lock.json
generated
@ -49,6 +49,7 @@
|
||||
"medium-zoom": "1.0.8",
|
||||
"nostr-tools": "1.15.0",
|
||||
"photoswipe": "5.4.3",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
"qr-code-styling": "^1.6.0-rc.1",
|
||||
"remark-directive": "^3.0.0",
|
||||
"sass": "1.67.0",
|
||||
@ -8645,6 +8646,11 @@
|
||||
"node": ">= 0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/photoswipe-dynamic-caption-plugin": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz",
|
||||
"integrity": "sha512-5XXdXLf2381nwe7KqQvcyStiUBi9TitYXppUQTrzPwYAi4lZsmWNnNKMclM7I4QGlX6fXo42v3bgb6rlK9pY1Q=="
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
|
@ -62,6 +62,7 @@
|
||||
"medium-zoom": "1.0.8",
|
||||
"nostr-tools": "1.15.0",
|
||||
"photoswipe": "5.4.3",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
"qr-code-styling": "^1.6.0-rc.1",
|
||||
"remark-directive": "^3.0.0",
|
||||
"sass": "1.67.0",
|
||||
|
@ -186,7 +186,7 @@ const ArticlePreview: Component<{
|
||||
});
|
||||
|
||||
const openReactionModal = (openOn = 'likes') => {
|
||||
app?.actions.openReactionModal(props.article.id, {
|
||||
app?.actions.openReactionModal(props.article.naddr, {
|
||||
likes: reactionsState.likes,
|
||||
zaps: reactionsState.zapCount,
|
||||
reposts: reactionsState.reposts,
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { A } from '@solidjs/router';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Component, createMemo, JSXElement, Show } from 'solid-js';
|
||||
import { batch, Component, createMemo, JSXElement, Show } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import { CustomZapInfo, useAppContext } from '../../contexts/AppContext';
|
||||
import { useMediaContext } from '../../contexts/MediaContext';
|
||||
import { useThreadContext } from '../../contexts/ThreadContext';
|
||||
import { date } from '../../lib/dates';
|
||||
import { trimVerification } from '../../lib/profile';
|
||||
import { nip05Verification, userName } from '../../stores/profile';
|
||||
import { note as t } from '../../translations';
|
||||
import { PrimalNote, PrimalUser } from '../../types/primal';
|
||||
import { PrimalNote, PrimalUser, ZapOption } from '../../types/primal';
|
||||
import Avatar from '../Avatar/Avatar';
|
||||
import { NoteReactionsState } from '../Note/Note';
|
||||
import NoteFooter from '../Note/NoteFooter/NoteFooter';
|
||||
import ParsedNote from '../ParsedNote/ParsedNote';
|
||||
import VerificationCheck from '../VerificationCheck/VerificationCheck';
|
||||
|
||||
@ -26,9 +31,33 @@ const EmbeddedNote: Component<{
|
||||
|
||||
const threadContext = useThreadContext();
|
||||
const intl = useIntl();
|
||||
const app = useAppContext();
|
||||
const account = useAccountContext();
|
||||
|
||||
let noteContent: HTMLDivElement | undefined;
|
||||
|
||||
const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
|
||||
likes: props.note?.post.likes || 0,
|
||||
liked: props.note?.post.noteActions?.liked || false,
|
||||
reposts: props.note?.post.reposts || 0,
|
||||
reposted: props.note?.post.noteActions?.reposted || false,
|
||||
replies: props.note?.post.replies || 0,
|
||||
replied: props.note?.post.noteActions?.replied || false,
|
||||
zapCount: props.note?.post.zaps || 0,
|
||||
satsZapped: props.note?.post.satszapped || 0,
|
||||
zapped: props.note?.post.noteActions?.zapped || false,
|
||||
zappedAmount: 0,
|
||||
zappedNow: false,
|
||||
isZapping: false,
|
||||
showZapAnim: false,
|
||||
hideZapIcon: false,
|
||||
moreZapsAvailable: false,
|
||||
isRepostMenuVisible: false,
|
||||
topZaps: [],
|
||||
topZapsFeed: [],
|
||||
quoteCount: 0,
|
||||
});
|
||||
|
||||
const noteId = () => nip19.noteEncode(props.note?.post.id);
|
||||
|
||||
const navToThread = () => {
|
||||
@ -75,6 +104,82 @@ const EmbeddedNote: Component<{
|
||||
);
|
||||
};
|
||||
|
||||
const onConfirmZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
batch(() => {
|
||||
updateReactionsState('zappedAmount', () => zapOption.amount || 0);
|
||||
updateReactionsState('satsZapped', (z) => z + (zapOption.amount || 0));
|
||||
updateReactionsState('zapped', () => true);
|
||||
updateReactionsState('showZapAnim', () => true)
|
||||
});
|
||||
|
||||
// addTopZap(zapOption);
|
||||
// addTopZapFeed(zapOption)
|
||||
};
|
||||
|
||||
const onSuccessZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
app?.actions.resetCustomZap();
|
||||
|
||||
const pubkey = account?.publicKey;
|
||||
|
||||
if (!pubkey) return;
|
||||
|
||||
batch(() => {
|
||||
updateReactionsState('zapCount', (z) => z + 1);
|
||||
updateReactionsState('isZapping', () => false);
|
||||
updateReactionsState('showZapAnim', () => false);
|
||||
updateReactionsState('hideZapIcon', () => false);
|
||||
updateReactionsState('zapped', () => true);
|
||||
});
|
||||
};
|
||||
|
||||
const onFailZap = (zapOption: ZapOption) => {
|
||||
const note = props.note;
|
||||
if (!note) return;
|
||||
|
||||
app?.actions.closeCustomZapModal();
|
||||
app?.actions.resetCustomZap();
|
||||
batch(() => {
|
||||
updateReactionsState('zappedAmount', () => -(zapOption.amount || 0));
|
||||
updateReactionsState('satsZapped', (z) => z - (zapOption.amount || 0));
|
||||
updateReactionsState('isZapping', () => false);
|
||||
updateReactionsState('showZapAnim', () => false);
|
||||
updateReactionsState('hideZapIcon', () => false);
|
||||
updateReactionsState('zapped', () => note.post.noteActions.zapped);
|
||||
});
|
||||
|
||||
// removeTopZap(zapOption);
|
||||
// removeTopZapFeed(zapOption);
|
||||
};
|
||||
|
||||
const onCancelZap = (zapOption: ZapOption) => {
|
||||
const note = props.note;
|
||||
if (!note) return;
|
||||
|
||||
app?.actions.closeCustomZapModal();
|
||||
app?.actions.resetCustomZap();
|
||||
batch(() => {
|
||||
updateReactionsState('zappedAmount', () => -(zapOption.amount || 0));
|
||||
updateReactionsState('satsZapped', (z) => z - (zapOption.amount || 0));
|
||||
updateReactionsState('isZapping', () => false);
|
||||
updateReactionsState('showZapAnim', () => false);
|
||||
updateReactionsState('hideZapIcon', () => false);
|
||||
updateReactionsState('zapped', () => note.post.noteActions.zapped);
|
||||
});
|
||||
|
||||
// removeTopZap(zapOption);
|
||||
// removeTopZapFeed(zapOption);
|
||||
};
|
||||
|
||||
const customZapInfo: () => CustomZapInfo = () => ({
|
||||
note: props.note,
|
||||
onConfirm: onConfirmZap,
|
||||
onSuccess: onSuccessZap,
|
||||
onFail: onFailZap,
|
||||
onCancel: onCancelZap,
|
||||
});
|
||||
|
||||
return wrapper(
|
||||
<>
|
||||
<div class={styles.mentionedNoteHeader}>
|
||||
@ -123,6 +228,15 @@ const EmbeddedNote: Component<{
|
||||
margins={2}
|
||||
/>
|
||||
</div>
|
||||
<div class={styles.footer}>
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={reactionsState}
|
||||
size="compact"
|
||||
updateState={updateReactionsState}
|
||||
customZapInfo={customZapInfo()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -128,6 +128,10 @@
|
||||
border-bottom: none;
|
||||
padding-inline: 0;
|
||||
z-index: var(--z-index-header);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 608px;
|
||||
}
|
||||
|
||||
|
||||
@ -221,6 +225,52 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
>button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
height: 40px;
|
||||
color: var(--text-primary-button);
|
||||
background: var(--accent);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding-block: 0;
|
||||
padding-left: 4px;
|
||||
margin: 0;
|
||||
text-transform: lowercase;
|
||||
|
||||
.avatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
.avatar {
|
||||
border: solid 2px var(--text-primary-button);
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
transition: margin-right 0.2s;
|
||||
margin-right: -9px;
|
||||
}
|
||||
}
|
||||
|
||||
.counter {
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.newArticleContentNotification {
|
||||
width: var(--central-content-width);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
|
||||
>button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -27,6 +27,8 @@ const ReadsHeader: Component< {
|
||||
} > = (props) => {
|
||||
|
||||
const reads = useReadsContext();
|
||||
const account = useAccountContext();
|
||||
const intl = useIntl();
|
||||
|
||||
let lastScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
|
||||
|
||||
@ -68,11 +70,61 @@ const ReadsHeader: Component< {
|
||||
onCleanup(() => {
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
});
|
||||
|
||||
return (
|
||||
<div id={props.id}>
|
||||
<div class={`${styles.bigFeedSelect} ${styles.readsFeed}`}>
|
||||
<ReedSelect big={true} />
|
||||
|
||||
<Show
|
||||
when={props.hasNewPosts()}
|
||||
>
|
||||
<button
|
||||
class={styles.newContentItem}
|
||||
onClick={props.loadNewContent}
|
||||
>
|
||||
<div class={styles.counter}>
|
||||
{intl.formatMessage(
|
||||
feedNewPosts,
|
||||
{
|
||||
number: props.newPostCount() >= 99 ? 99 : props.newPostCount(),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={props.hasNewPosts() && !account?.showNewNoteForm && !((reads?.scrollTop || 0) < 85)}
|
||||
>
|
||||
<div class={styles.newArticleContentNotification}>
|
||||
<button
|
||||
onClick={props.loadNewContent}
|
||||
>
|
||||
<div class={styles.avatars}>
|
||||
<For each={props.newPostAuthors}>
|
||||
{(user) => (
|
||||
<div
|
||||
class={styles.avatar}
|
||||
title={userName(user)}
|
||||
>
|
||||
<Avatar user={user} size="xss" />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class={styles.counter}>
|
||||
{intl.formatMessage(
|
||||
feedNewPosts,
|
||||
{
|
||||
number: props.newPostCount(),
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -243,8 +243,6 @@ const ArticleFooter: Component<{
|
||||
newTop = -6;
|
||||
}
|
||||
|
||||
console.log('SIZE: ', newLeft);
|
||||
|
||||
medZapAnimation.style.left = `${newLeft}px`;
|
||||
medZapAnimation.style.top = `${newTop}px`;
|
||||
|
||||
|
@ -95,6 +95,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
grid-template-columns: 138px 138px 138px 138px auto;
|
||||
|
||||
.bookmarkFoot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.normal {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -27,11 +27,11 @@ export const lottieDuration = () => zapMD.op * 1_000 / zapMD.fr;
|
||||
|
||||
const NoteFooter: Component<{
|
||||
note: PrimalNote,
|
||||
size?: 'xwide' | 'wide' | 'normal' | 'short',
|
||||
size?: 'xwide' | 'wide' | 'normal' | 'compact' | 'short',
|
||||
id?: string,
|
||||
state: NoteReactionsState,
|
||||
updateState: SetStoreFunction<NoteReactionsState>,
|
||||
customZapInfo: CustomZapInfo,
|
||||
updateState?: SetStoreFunction<NoteReactionsState>,
|
||||
customZapInfo?: CustomZapInfo,
|
||||
large?: boolean,
|
||||
onZapAnim?: (zapOption: ZapOption) => void,
|
||||
}> = (props) => {
|
||||
@ -66,7 +66,8 @@ const NoteFooter: Component<{
|
||||
|
||||
const onClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
!document?.getElementById(`repost_menu_${props.note.post.id}`)?.contains(e.target as Node)
|
||||
!document?.getElementById(`repost_menu_${props.note.post.id}`)?.contains(e.target as Node) &&
|
||||
props.updateState
|
||||
) {
|
||||
props.updateState('isRepostMenuVisible', () => false);
|
||||
}
|
||||
@ -83,7 +84,7 @@ const NoteFooter: Component<{
|
||||
|
||||
const showRepostMenu = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
props.updateState('isRepostMenuVisible', () => true);
|
||||
props.updateState && props.updateState('isRepostMenuVisible', () => true);
|
||||
};
|
||||
|
||||
const doQuote = () => {
|
||||
@ -91,7 +92,7 @@ const NoteFooter: Component<{
|
||||
account?.actions.showGetStarted();
|
||||
return;
|
||||
}
|
||||
props.updateState('isRepostMenuVisible', () => false);
|
||||
props.updateState && props.updateState('isRepostMenuVisible', () => false);
|
||||
account?.actions?.quoteNote(`nostr:${props.note.post.noteId}`);
|
||||
account?.actions?.showNewNoteForm();
|
||||
};
|
||||
@ -113,14 +114,14 @@ const NoteFooter: Component<{
|
||||
return;
|
||||
}
|
||||
|
||||
props.updateState('isRepostMenuVisible', () => false);
|
||||
props.updateState && props.updateState('isRepostMenuVisible', () => false);
|
||||
|
||||
const { success } = await sendRepost(props.note, account.relays, account.relaySettings);
|
||||
|
||||
if (success) {
|
||||
batch(() => {
|
||||
props.updateState('reposts', (r) => r + 1);
|
||||
props.updateState('reposted', () => true);
|
||||
props.updateState && props.updateState('reposts', (r) => r + 1);
|
||||
props.updateState && props.updateState('reposted', () => true);
|
||||
});
|
||||
|
||||
toast?.sendSuccess(
|
||||
@ -160,8 +161,8 @@ const NoteFooter: Component<{
|
||||
|
||||
if (success) {
|
||||
batch(() => {
|
||||
props.updateState('likes', (l) => l + 1);
|
||||
props.updateState('liked', () => true);
|
||||
props.updateState && props.updateState('likes', (l) => l + 1);
|
||||
props.updateState && props.updateState('liked', () => true);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -172,7 +173,7 @@ const NoteFooter: Component<{
|
||||
|
||||
if (!account?.hasPublicKey()) {
|
||||
account?.actions.showGetStarted();
|
||||
props.updateState('isZapping', () => false);
|
||||
props.updateState && props.updateState('isZapping', () => false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -187,13 +188,13 @@ const NoteFooter: Component<{
|
||||
toast?.sendWarning(
|
||||
intl.formatMessage(t.zapUnavailable),
|
||||
);
|
||||
props.updateState('isZapping', () => false);
|
||||
props.updateState && props.updateState('isZapping', () => false);
|
||||
return;
|
||||
}
|
||||
|
||||
quickZapDelay = setTimeout(() => {
|
||||
app?.actions.openCustomZapModal(props.customZapInfo);
|
||||
props.updateState('isZapping', () => true);
|
||||
props.customZapInfo && app?.actions.openCustomZapModal(props.customZapInfo);
|
||||
props.updateState && props.updateState('isZapping', () => true);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
@ -219,7 +220,7 @@ const NoteFooter: Component<{
|
||||
|
||||
const animateZap = () => {
|
||||
setTimeout(() => {
|
||||
props.updateState('hideZapIcon', () => true);
|
||||
props.updateState && props.updateState('hideZapIcon', () => true);
|
||||
|
||||
if (!medZapAnimation) {
|
||||
return;
|
||||
@ -243,14 +244,19 @@ const NoteFooter: Component<{
|
||||
newTop = -6;
|
||||
}
|
||||
|
||||
if (size() === 'compact') {
|
||||
newLeft = 26;
|
||||
newTop = -6;
|
||||
}
|
||||
|
||||
medZapAnimation.style.left = `${newLeft}px`;
|
||||
medZapAnimation.style.top = `${newTop}px`;
|
||||
|
||||
const onAnimDone = () => {
|
||||
batch(() => {
|
||||
props.updateState('showZapAnim', () => false);
|
||||
props.updateState('hideZapIcon', () => false);
|
||||
props.updateState('zapped', () => true);
|
||||
props.updateState && props.updateState('showZapAnim', () => false);
|
||||
props.updateState && props.updateState('hideZapIcon', () => false);
|
||||
props.updateState && props.updateState('zapped', () => true);
|
||||
});
|
||||
medZapAnimation?.removeEventListener('complete', onAnimDone);
|
||||
}
|
||||
@ -280,9 +286,9 @@ const NoteFooter: Component<{
|
||||
const emoji = settings?.defaultZap.emoji;
|
||||
|
||||
batch(() => {
|
||||
props.updateState('isZapping', () => true);
|
||||
props.updateState('satsZapped', (z) => z + amount);
|
||||
props.updateState('showZapAnim', () => true);
|
||||
props.updateState && props.updateState('isZapping', () => true);
|
||||
props.updateState && props.updateState('satsZapped', (z) => z + amount);
|
||||
props.updateState && props.updateState('showZapAnim', () => true);
|
||||
});
|
||||
|
||||
props.onZapAnim && props.onZapAnim({ amount, message, emoji })
|
||||
@ -290,10 +296,10 @@ const NoteFooter: Component<{
|
||||
setTimeout(async () => {
|
||||
const success = await zapNote(props.note, account.publicKey, amount, message, account.relays);
|
||||
|
||||
props.updateState('isZapping', () => false);
|
||||
props.updateState && props.updateState('isZapping', () => false);
|
||||
|
||||
if (success) {
|
||||
props.customZapInfo.onSuccess({
|
||||
props.customZapInfo &&props.customZapInfo.onSuccess({
|
||||
emoji,
|
||||
amount,
|
||||
message,
|
||||
@ -302,7 +308,7 @@ const NoteFooter: Component<{
|
||||
return;
|
||||
}
|
||||
|
||||
props.customZapInfo.onFail({
|
||||
props.customZapInfo && props.customZapInfo.onFail({
|
||||
emoji,
|
||||
amount,
|
||||
message,
|
||||
|
86
src/components/Note/NoteGallery.tsx
Normal file
86
src/components/Note/NoteGallery.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { Component, createEffect, createMemo, createSignal, onMount, Show } from 'solid-js';
|
||||
import { MediaVariant, PrimalNote } from '../../types/primal';
|
||||
|
||||
|
||||
import styles from './Note.module.scss';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { note as t } from '../../translations';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import MentionedUserLink from './MentionedUserLink/MentionedUserLink';
|
||||
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
// @ts-ignore
|
||||
import PhotoSwipeDynamicCaption from 'photoswipe-dynamic-caption-plugin';
|
||||
|
||||
import NoteImage from '../NoteImage/NoteImage';
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
import { generatePrivateKey } from 'nostr-tools';
|
||||
import { imageRegexG } from '../../constants';
|
||||
import { useMediaContext } from '../../contexts/MediaContext';
|
||||
import { createStore } from 'solid-js/store';
|
||||
|
||||
const NoteGallery: Component<{
|
||||
note: PrimalNote,
|
||||
id?: string,
|
||||
}> = (props) => {
|
||||
const intl = useIntl();
|
||||
const media = useMediaContext();
|
||||
|
||||
const id = generatePrivateKey();
|
||||
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
gallery: `#galleryimage_${id}`,
|
||||
children: `a.image_${props.note.post.noteId}`,
|
||||
showHideAnimationType: 'zoom',
|
||||
initialZoomLevel: 'fit',
|
||||
secondaryZoomLevel: 2,
|
||||
maxZoomLevel: 3,
|
||||
pswpModule: () => import('photoswipe'),
|
||||
});
|
||||
|
||||
const [store, setStore] = createStore<{ url: string, image: MediaVariant | undefined}>({
|
||||
url: '',
|
||||
image: undefined,
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const urls = props.note.content.matchAll(imageRegexG);
|
||||
let result = urls.next();
|
||||
let images: string[] = [];
|
||||
|
||||
while (!result.done) {
|
||||
images.push(result.value[0]);
|
||||
result = urls.next();
|
||||
}
|
||||
|
||||
let url = images[0];
|
||||
|
||||
let image = media?.actions.getMedia(url, 'o');
|
||||
url = image?.media_url || url;
|
||||
|
||||
setStore(() => ({ url, image }));
|
||||
|
||||
const captionPlugin = new PhotoSwipeDynamicCaption(lightbox, {
|
||||
// Plugins options, for example:
|
||||
type: 'auto',
|
||||
captionContent: '.pswp-caption-content'
|
||||
});
|
||||
|
||||
lightbox.init();
|
||||
});
|
||||
|
||||
return (
|
||||
<div id={`galleryimage_${id}`} data-note={props.note.id} data-url={store.url}>
|
||||
<NoteImage
|
||||
class={`noteimage image_${props.note.post.noteId} cell_${1}`}
|
||||
src={store.url}
|
||||
media={store.image}
|
||||
width={210}
|
||||
shortHeight={true}
|
||||
plainBorder={true}
|
||||
caption={props.note.content.replace(imageRegexG, '')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default hookForDev(NoteGallery);
|
@ -16,6 +16,7 @@ const NoteImage: Component<{
|
||||
onImageLoaded?: (url: string | undefined) => void,
|
||||
shortHeight?: boolean,
|
||||
plainBorder?: boolean,
|
||||
caption?: string,
|
||||
}> = (props) => {
|
||||
const imgId = generatePrivateKey();
|
||||
|
||||
@ -142,6 +143,7 @@ const NoteImage: Component<{
|
||||
onerror={onError}
|
||||
width={willBeTooBig() ? undefined : `${props.width || 524}px`}
|
||||
/>
|
||||
<div class="pswp-caption-content">{props.caption}</div>
|
||||
</a>
|
||||
</Show>
|
||||
);
|
||||
|
@ -53,6 +53,7 @@ import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||
import Lnbc from '../Lnbc/Lnbc';
|
||||
import { subscribeTo } from '../../sockets';
|
||||
import { APP_ID } from '../../App';
|
||||
import { logError } from '../../lib/logger';
|
||||
|
||||
const groupGridLimit = 7;
|
||||
|
||||
@ -1009,6 +1010,7 @@ const ParsedNote: Component<{
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
logError('ERROR rendering note mention', e);
|
||||
setWordsDisplayed(w => w + 1);
|
||||
link = <span class={styles.error}>{token}</span>;
|
||||
}
|
||||
|
@ -48,8 +48,8 @@
|
||||
align-items: center;
|
||||
.statNumber {
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@ -281,3 +281,25 @@
|
||||
max-width: 80% !important;
|
||||
}
|
||||
}
|
||||
|
||||
// .galleryGrid {
|
||||
// display: grid;
|
||||
// grid-template-columns: 210px 210px 210px;
|
||||
// grid-column-gap: 5px;
|
||||
// }
|
||||
|
||||
.galleryGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 640px;
|
||||
|
||||
.galleryColumn {
|
||||
flex: 33%;
|
||||
max-width: 33%;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
// >div {
|
||||
// flex-grow: 1;
|
||||
// }
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useIntl } from "@cookbook/solid-intl";
|
||||
import { Tabs } from "@kobalte/core";
|
||||
import { A } from "@solidjs/router";
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||
import { Component, createEffect, createSignal, For, Match, onMount, Show, Switch } from "solid-js";
|
||||
import { createStore, unwrap } from "solid-js/store";
|
||||
import { profileContactListPage } from "../../constants";
|
||||
import { imageRegex, imageRegexG, profileContactListPage } from "../../constants";
|
||||
import { useAccountContext } from "../../contexts/AccountContext";
|
||||
import { useMediaContext } from "../../contexts/MediaContext";
|
||||
import { useProfileContext } from "../../contexts/ProfileContext";
|
||||
import { date } from "../../lib/dates";
|
||||
import { hookForDev } from "../../lib/devTools";
|
||||
@ -18,10 +20,14 @@ import Avatar from "../Avatar/Avatar";
|
||||
import ButtonCopy from "../Buttons/ButtonCopy";
|
||||
import Loader from "../Loader/Loader";
|
||||
import Note from "../Note/Note";
|
||||
import NoteImage from "../NoteImage/NoteImage";
|
||||
import Paginator from "../Paginator/Paginator";
|
||||
import ProfileContact from "../ProfileContact/ProfileContact";
|
||||
// @ts-ignore Bad types in nostr-tools
|
||||
import { generatePrivateKey } from 'nostr-tools';
|
||||
|
||||
import styles from "./ProfileTabs.module.scss";
|
||||
import NoteGallery from "../Note/NoteGallery";
|
||||
|
||||
|
||||
const ProfileTabs: Component<{
|
||||
@ -32,6 +38,7 @@ const ProfileTabs: Component<{
|
||||
const intl = useIntl();
|
||||
const profile = useProfileContext();
|
||||
const account = useAccountContext();
|
||||
const media = useMediaContext();
|
||||
|
||||
const [currentTab, setCurrentTab] = createSignal<string>('notes');
|
||||
|
||||
@ -143,6 +150,9 @@ const ProfileTabs: Component<{
|
||||
case 'replies':
|
||||
profile.replies.length === 0 && profile.actions.fetchReplies(profile.profileKey);
|
||||
break;
|
||||
case 'gallery':
|
||||
profile.gallery.length === 0 && profile.actions.fetchGallery(profile.profileKey);
|
||||
break;
|
||||
case 'follows':
|
||||
profile.contacts.length === 0 && profile.actions.fetchContactList(profile.profileKey);
|
||||
break;
|
||||
@ -158,6 +168,13 @@ const ProfileTabs: Component<{
|
||||
}
|
||||
};
|
||||
|
||||
const galleryImages = () => {
|
||||
return profile?.gallery.filter(note => {
|
||||
const test = (imageRegex).test(note.content);
|
||||
return test;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={profile && profile.fetchedUserStats}
|
||||
@ -177,6 +194,7 @@ const ProfileTabs: Component<{
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
</Show>
|
||||
|
||||
<Tabs.Trigger class={styles.profileTab} value="notes">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
@ -199,6 +217,14 @@ const ProfileTabs: Component<{
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
|
||||
<Tabs.Trigger class={styles.profileTab} value="gallery">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statName}>
|
||||
{intl.formatMessage(t.stats.gallery)}
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Trigger>
|
||||
|
||||
<Tabs.Trigger class={styles.profileTab} value="zaps">
|
||||
<div class={styles.stat}>
|
||||
<div class={styles.statNumber}>
|
||||
@ -415,6 +441,85 @@ const ProfileTabs: Component<{
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="gallery">
|
||||
<div class={styles.profileNotes}>
|
||||
<Switch
|
||||
fallback={
|
||||
<div style="margin-top: 40px;">
|
||||
<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.gallery.length === 0 && !profile.isFetchingGallery}>
|
||||
<div class={styles.mutedProfile}>
|
||||
{intl.formatMessage(
|
||||
t.noReplies,
|
||||
{ name: profile?.userProfile ? userName(profile?.userProfile) : profile?.profileKey },
|
||||
)}
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={profile && profile.gallery.length > 0}>
|
||||
<div class={styles.galleryGrid}>
|
||||
<div class={styles.galleryColumn}>
|
||||
<For each={galleryImages()}>
|
||||
{(note, index) => (
|
||||
<Show when={index()%3 === 1}>
|
||||
<NoteGallery note={note} />
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class={styles.galleryColumn}>
|
||||
<For each={galleryImages()}>
|
||||
{(note, index) => (
|
||||
<Show when={index()%3 === 2}>
|
||||
<NoteGallery note={note} />
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class={styles.galleryColumn}>
|
||||
<For each={galleryImages()}>
|
||||
{(note, index) => (
|
||||
<Show when={index()%3 === 0}>
|
||||
<NoteGallery note={note} />
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<Paginator
|
||||
loadNextPage={() => {
|
||||
profile?.actions.fetchNextGalleryPage();
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content class={styles.tabContent} value="follows">
|
||||
<div class={styles.profileNotes}>
|
||||
<Show
|
||||
|
@ -267,6 +267,7 @@ const ReactionsModal: Component<{
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
topZaps: {},
|
||||
};
|
||||
|
||||
const unsub = subscribeTo(subId, (type,_, content) => {
|
||||
@ -355,7 +356,7 @@ const ReactionsModal: Component<{
|
||||
});
|
||||
|
||||
setIsFetching(() => true);
|
||||
getEventQuotes(props.noteId, subId, offset);
|
||||
getEventQuotes(props.noteId, subId, offset, account?.publicKey);
|
||||
};
|
||||
|
||||
const getQuoteCount = () => {
|
||||
|
@ -278,6 +278,8 @@ export const profileRegexG = /nostr:((npub|nprofile)1\w+)\b/g;
|
||||
export const addrRegex = /nostr:((naddr)1\w+)\b/;
|
||||
export const addrRegexG = /nostr:((naddr)1\w+)\b/g;
|
||||
export const editMentionRegex = /(?:\s|^)@\`(.*?)\`/ig;
|
||||
export const imageRegex = /(http(s?):)([/|.|\w|\s|-])*\.(?:png|jpg|jpeg|webp|gif|format=png)/;
|
||||
export const imageRegexG = /(http(s?):)([/|.|\w|\s|-])*\.(?:png|jpg|jpeg|webp|gif|format=png)/g;
|
||||
|
||||
export const specialCharsRegex = /[^A-Za-z0-9]/;
|
||||
export const hashtagCharsRegex = /[^A-Za-z0-9\-\_]/;
|
||||
|
@ -53,7 +53,7 @@ import { setLinkPreviews } from "../lib/notes";
|
||||
import { subscribeTo } from "../sockets";
|
||||
import { parseBolt11 } from "../utils";
|
||||
import { readRecomendedUsers, saveRecomendedUsers } from "../lib/localStore";
|
||||
import { fetchUserArticles } from "../handleNotes";
|
||||
import { fetchUserArticles, fetchUserGallery } from "../handleNotes";
|
||||
|
||||
export type UserStats = {
|
||||
pubkey: string,
|
||||
@ -78,6 +78,7 @@ export type ProfileContextStore = {
|
||||
notes: PrimalNote[],
|
||||
replies: PrimalNote[],
|
||||
zaps: PrimalZap[],
|
||||
gallery: PrimalNote[],
|
||||
zapListOffset: number,
|
||||
lastZap: PrimalZap | undefined,
|
||||
future: {
|
||||
@ -91,12 +92,14 @@ export type ProfileContextStore = {
|
||||
isFetching: boolean,
|
||||
isProfileFetched: boolean,
|
||||
isFetchingReplies: boolean,
|
||||
isFetchingGallery: boolean,
|
||||
page: FeedPage,
|
||||
repliesPage: FeedPage,
|
||||
reposts: Record<string, string> | undefined,
|
||||
lastNote: PrimalNote | undefined,
|
||||
lastArticle: PrimalArticle | undefined,
|
||||
lastReply: PrimalNote | undefined,
|
||||
lastGallery: PrimalNote | undefined,
|
||||
following: string[],
|
||||
sidebar: FeedPage & { notes: PrimalNote[] },
|
||||
filterReason: { action: 'block' | 'allow', pubkey?: string, group?: string } | null,
|
||||
@ -126,6 +129,9 @@ export type ProfileContextStore = {
|
||||
fetchArticles: (noteId: string | undefined, until?: number) => void,
|
||||
fetchNextArticlesPage: () => void,
|
||||
clearArticles: () => void,
|
||||
fetchGallery: (noteId: string | undefined, until?: number) => void,
|
||||
fetchNextGalleryPage: () => void,
|
||||
clearGallery: () => void,
|
||||
updatePage: (content: NostrEventContent) => void,
|
||||
savePage: (page: FeedPage) => void,
|
||||
updateRepliesPage: (content: NostrEventContent) => void,
|
||||
@ -167,11 +173,13 @@ export const initialData = {
|
||||
articles: [],
|
||||
notes: [],
|
||||
replies: [],
|
||||
gallery: [],
|
||||
isFetching: false,
|
||||
isProfileFetched: false,
|
||||
isFetchingReplies: false,
|
||||
isProfileFollowing: false,
|
||||
isFetchingZaps: false,
|
||||
isFetchingGallery: false,
|
||||
page: { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {}, topZaps: {} },
|
||||
repliesPage: { messages: [], users: {}, postStats: {}, mentions: {}, noteActions: {}, topZaps: {} },
|
||||
reposts: {},
|
||||
@ -182,6 +190,7 @@ export const initialData = {
|
||||
lastArticle: undefined,
|
||||
lastReply: undefined,
|
||||
lastZap: undefined,
|
||||
lastGallery: undefined,
|
||||
following: [],
|
||||
filterReason: null,
|
||||
contacts: [],
|
||||
@ -509,6 +518,45 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
getUserFeed(account?.publicKey, pubkey, `profile_replies_${APP_ID}`, 'replies', until, limit);
|
||||
}
|
||||
|
||||
const fetchGallery = async (pubkey: string | undefined, until = 0, limit = 20) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('isFetchingGallery', () => true);
|
||||
let gallery = await fetchUserGallery(account?.publicKey, pubkey, 'user_media_thumbnails', `profile_gallery_${APP_ID}`, until, limit);
|
||||
|
||||
updateStore('gallery', (arts) => [ ...arts, ...gallery]);
|
||||
updateStore('isFetchingGallery', () => false);
|
||||
}
|
||||
|
||||
const fetchNextGalleryPage = () => {
|
||||
const lastNote = store.gallery[store.gallery.length - 1];
|
||||
|
||||
if (!lastNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStore('lastGallery', () => ({ ...lastNote }));
|
||||
|
||||
const criteria = paginationPlan('latest');
|
||||
|
||||
const noteData: Record<string, any> = lastNote.repost ?
|
||||
lastNote.repost.note :
|
||||
lastNote.post;
|
||||
|
||||
const until = noteData[criteria];
|
||||
|
||||
if (until > 0 && store.profileKey) {
|
||||
fetchGallery(store.profileKey, until);
|
||||
}
|
||||
};
|
||||
|
||||
const clearGallery = () => {
|
||||
updateStore('gallery', () => []);
|
||||
updateStore('lastGallery', () => undefined);
|
||||
};
|
||||
|
||||
const clearNotes = () => {
|
||||
updateStore('page', () => ({ messages: [], users: {}, postStats: {}, noteActions: {} }));
|
||||
updateStore('notes', () => []);
|
||||
@ -1411,6 +1459,9 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
|
||||
fetchArticles,
|
||||
fetchNextArticlesPage,
|
||||
clearArticles,
|
||||
fetchGallery,
|
||||
fetchNextGalleryPage,
|
||||
clearGallery,
|
||||
updatePage,
|
||||
savePage,
|
||||
saveReplies,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Kind } from "./constants";
|
||||
import { getEvents, getUserArticleFeed } from "./lib/feed";
|
||||
import { getEvents, getUserArticleFeed, getUserFeed } from "./lib/feed";
|
||||
import { decodeIdentifier, hexToNpub } from "./lib/keys";
|
||||
import { getParametrizedEvents, setLinkPreviews } from "./lib/notes";
|
||||
import { getUserProfileInfo } from "./lib/profile";
|
||||
@ -781,3 +781,188 @@ export const fetchUserProfile = (userPubkey: string | undefined, pubkey: string
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const fetchUserGallery = (userPubkey: string | undefined, pubkey: string | undefined, type: 'authored' | 'replies' | 'bookmarks' | 'user_media_thumbnails', subId: string, until = 0, limit = 10) => {
|
||||
return new Promise<PrimalNote[]>((resolve, reject) => {
|
||||
if (!pubkey) reject('Missing pubkey');
|
||||
|
||||
let page: FeedPage = {
|
||||
users: {},
|
||||
messages: [],
|
||||
postStats: {},
|
||||
mentions: {},
|
||||
noteActions: {},
|
||||
relayHints: {},
|
||||
topZaps: {},
|
||||
since: 0,
|
||||
until: 0,
|
||||
wordCount: {},
|
||||
}
|
||||
|
||||
let lastNote: PrimalNote | undefined;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, _, content) => {
|
||||
|
||||
if (type === 'EOSE') {
|
||||
unsub();
|
||||
const notes = convertToNotes(page, page.topZaps);
|
||||
|
||||
resolve(notes);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'EVENT') {
|
||||
if (!content) return;
|
||||
updatePage(content);
|
||||
}
|
||||
});
|
||||
|
||||
getUserFeed(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.Text, 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;
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
@ -437,3 +437,57 @@ body::after{
|
||||
button:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// PhotSwipe CSS
|
||||
.pswp__dynamic-caption {
|
||||
color: var(--text-primary);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transition: opacity 120ms linear !important; /* override default */
|
||||
}
|
||||
|
||||
.pswp-caption-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pswp__dynamic-caption a {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.pswp__dynamic-caption--faded {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.pswp__dynamic-caption--aside {
|
||||
width: auto;
|
||||
max-width: 300px;
|
||||
padding: 20px 15px 20px 20px;
|
||||
margin-top: 70px;
|
||||
}
|
||||
|
||||
.pswp__dynamic-caption--below {
|
||||
width: auto;
|
||||
max-width: 700px;
|
||||
padding: 15px 0 0;
|
||||
}
|
||||
|
||||
.pswp__dynamic-caption--on-hor-edge {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.pswp__dynamic-caption--mobile {
|
||||
width: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
padding: 10px 15px;
|
||||
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
/* override styles that were set via JS.
|
||||
as they interfere with size measurement */
|
||||
top: auto !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ export const getEvents = (user_pubkey: string | undefined, eventIds: string[], s
|
||||
|
||||
};
|
||||
|
||||
export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, notes: 'authored' | 'replies' | 'bookmarks', until = 0, limit = 20, offset = 0) => {
|
||||
export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | undefined, subid: string, notes: 'authored' | 'replies' | 'bookmarks' | 'user_media_thumbnails', until = 0, limit = 20, offset = 0) => {
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
@ -126,7 +126,7 @@ export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | un
|
||||
let payload: {
|
||||
pubkey: string,
|
||||
limit: number,
|
||||
notes: 'authored' | 'replies' | 'bookmarks',
|
||||
notes: 'authored' | 'replies' | 'bookmarks' | 'user_media_thumbnails',
|
||||
user_pubkey?: string,
|
||||
until?: number,
|
||||
offset?: number,
|
||||
|
@ -6,7 +6,7 @@ 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 { npubToHex } from "./keys";
|
||||
import { decodeIdentifier, npubToHex } from "./keys";
|
||||
import { logError, logInfo, logWarning } from "./logger";
|
||||
import { getMediaUrl as getMediaUrlDefault } from "./media";
|
||||
import { signEvent } from "./nostrAPI";
|
||||
@ -556,34 +556,120 @@ export const triggerImportEvents = (events: NostrRelaySignedEvent[], subId: stri
|
||||
|
||||
|
||||
export const getEventReactions = (eventId: string, kind: number, subid: string, offset = 0) => {
|
||||
const event_id = eventId.startsWith('note1') ? npubToHex(eventId) : eventId;
|
||||
let event_id: string | undefined = eventId;
|
||||
let pubkey: string | undefined;
|
||||
let identifier: string | undefined;
|
||||
|
||||
if (eventId.startsWith('note1')) {
|
||||
event_id = npubToHex(eventId);
|
||||
}
|
||||
|
||||
if (eventId.startsWith('naddr')) {
|
||||
const decode = decodeIdentifier(event_id);
|
||||
|
||||
pubkey = decode.data.pubkey;
|
||||
identifier = decode.data.identifier;
|
||||
event_id = undefined;
|
||||
}
|
||||
|
||||
let payload = {
|
||||
kind,
|
||||
limit: 20,
|
||||
offset,
|
||||
};
|
||||
|
||||
if (event_id) {
|
||||
// @ts-ignore
|
||||
payload.event_id = event_id;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
payload.pubkey = pubkey;
|
||||
// @ts-ignore
|
||||
payload.identifier = identifier;
|
||||
}
|
||||
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["event_actions", { event_id, kind, limit: 20, offset }]},
|
||||
{cache: ["event_actions", { ...payload }]},
|
||||
]));
|
||||
};
|
||||
|
||||
export const getEventQuotes = (eventId: string, subid: string, offset = 0) => {
|
||||
const event_id = eventId.startsWith('note1') ? npubToHex(eventId) : eventId;
|
||||
export const getEventQuotes = (eventId: string, subid: string, offset = 0, user_pubkey?: string | undefined) => {
|
||||
let event_id: string | undefined = eventId;
|
||||
let pubkey: string | undefined;
|
||||
let identifier: string | undefined;
|
||||
|
||||
if (eventId.startsWith('note1')) {
|
||||
event_id = npubToHex(eventId);
|
||||
}
|
||||
|
||||
if (eventId.startsWith('naddr')) {
|
||||
const decode = decodeIdentifier(event_id);
|
||||
|
||||
pubkey = decode.data.pubkey;
|
||||
identifier = decode.data.identifier;
|
||||
event_id = undefined;
|
||||
}
|
||||
|
||||
let payload = {
|
||||
imit: 20,
|
||||
offset,
|
||||
};
|
||||
|
||||
if (event_id) {
|
||||
// @ts-ignore
|
||||
payload.event_id = event_id;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
payload.pubkey = pubkey;
|
||||
// @ts-ignore
|
||||
payload.identifier = identifier;
|
||||
}
|
||||
|
||||
if (user_pubkey) {
|
||||
// @ts-ignore
|
||||
payload.user_pubkey = user_pubkey;
|
||||
}
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["note_mentions", { event_id, limit: 20, offset }]},
|
||||
{cache: ["note_mentions", { ...payload }]},
|
||||
]));
|
||||
};
|
||||
|
||||
export const getEventZaps = (eventId: string, user_pubkey: string | undefined, subid: string, limit: number, offset = 0) => {
|
||||
const event_id = eventId.startsWith('note1') ? npubToHex(eventId) : eventId;
|
||||
let event_id: string | undefined = eventId;
|
||||
let pubkey: string | undefined;
|
||||
let identifier: string | undefined;
|
||||
|
||||
if (eventId.startsWith('note1')) {
|
||||
event_id = npubToHex(eventId);
|
||||
}
|
||||
|
||||
if (eventId.startsWith('naddr')) {
|
||||
const decode = decodeIdentifier(event_id);
|
||||
|
||||
pubkey = decode.data.pubkey;
|
||||
identifier = decode.data.identifier;
|
||||
event_id = undefined;
|
||||
}
|
||||
|
||||
let payload = {
|
||||
event_id,
|
||||
limit,
|
||||
offset
|
||||
};
|
||||
|
||||
if (event_id) {
|
||||
// @ts-ignore
|
||||
payload.event_id = event_id;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
payload.pubkey = pubkey;
|
||||
// @ts-ignore
|
||||
payload.identifier = identifier;
|
||||
}
|
||||
|
||||
if (user_pubkey) {
|
||||
// @ts-ignore
|
||||
payload.user_pubkey = user_pubkey;
|
||||
|
@ -684,7 +684,7 @@ const Longform: Component< { naddr: string } > = (props) => {
|
||||
const openReactionModal = (openOn = 'likes') => {
|
||||
if (!store.article) return;
|
||||
|
||||
app?.actions.openReactionModal(store.article.id, {
|
||||
app?.actions.openReactionModal(store.article.naddr, {
|
||||
likes: reactionsState.likes,
|
||||
zaps: reactionsState.zapCount,
|
||||
reposts: reactionsState.reposts,
|
||||
|
@ -133,6 +133,7 @@ const Profile: Component = () => {
|
||||
profile?.actions.clearContacts();
|
||||
profile?.actions.clearZaps();
|
||||
profile?.actions.clearFilterReason();
|
||||
profile?.actions.clearGallery();
|
||||
setHasTiers(() => false);
|
||||
}
|
||||
|
||||
|
@ -170,10 +170,10 @@ const Home: Component = () => {
|
||||
when={params.topic}
|
||||
fallback={
|
||||
<ReadsHeader
|
||||
hasNewPosts={() => {}}
|
||||
loadNewContent={() => {}}
|
||||
newPostCount={() => {}}
|
||||
newPostAuthors={[]}
|
||||
hasNewPosts={hasNewPosts}
|
||||
loadNewContent={loadNewContent}
|
||||
newPostCount={newPostCount}
|
||||
newPostAuthors={newPostAuthors}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
@ -282,7 +282,7 @@ export const convertToNotes: ConvertToNotes = (page, topZaps) => {
|
||||
|
||||
if (mentionIds.length > 0) {
|
||||
for (let i = 0;i<mentionIds.length;i++) {
|
||||
const id = mentionIds[i];
|
||||
let id = mentionIds[i];
|
||||
const m = mentions && mentions[id];
|
||||
|
||||
if (!m) {
|
||||
@ -296,11 +296,36 @@ export const convertToNotes: ConvertToNotes = (page, topZaps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const mentionStat = page.postStats[id];
|
||||
|
||||
const noteActions = (page.noteActions && page.noteActions[id]) ?? {
|
||||
event_id: id,
|
||||
liked: false,
|
||||
replied: false,
|
||||
reposted: false,
|
||||
zapped: false,
|
||||
};
|
||||
|
||||
mentionedNotes[id] = {
|
||||
// @ts-ignore TODO: Investigate this typing
|
||||
post: { ...m, noteId: nip19.noteEncode(m.id) },
|
||||
post: {
|
||||
...m,
|
||||
noteId: nip19.noteEncode(m.id),
|
||||
likes: mentionStat?.likes || 0,
|
||||
mentions: mentionStat?.mentions || 0,
|
||||
reposts: mentionStat?.reposts || 0,
|
||||
replies: mentionStat?.replies || 0,
|
||||
zaps: mentionStat?.zaps || 0,
|
||||
score: mentionStat?.score || 0,
|
||||
score24h: mentionStat?.score24h || 0,
|
||||
satszapped: mentionStat?.satszapped || 0,
|
||||
noteActions,
|
||||
},
|
||||
user: convertToUser(page.users[m.pubkey] || emptyUser(m.pubkey)),
|
||||
mentionedUsers,
|
||||
pubkey: m.pubkey,
|
||||
id: m.id,
|
||||
noteId: nip19.noteEncode(m.id),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -432,11 +457,36 @@ export const convertToArticles: ConvertToArticles = (page, topZaps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const mentionStat = page.postStats[id];
|
||||
|
||||
const noteActions = (page.noteActions && page.noteActions[id]) ?? {
|
||||
event_id: id,
|
||||
liked: false,
|
||||
replied: false,
|
||||
reposted: false,
|
||||
zapped: false,
|
||||
};
|
||||
|
||||
mentionedNotes[id] = {
|
||||
// @ts-ignore TODO: Investigate this typing
|
||||
post: { ...m, noteId: nip19.noteEncode(m.id) },
|
||||
post: {
|
||||
...m,
|
||||
noteId: nip19.noteEncode(m.id),
|
||||
likes: mentionStat?.likes || 0,
|
||||
mentions: mentionStat?.mentions || 0,
|
||||
reposts: mentionStat?.reposts || 0,
|
||||
replies: mentionStat?.replies || 0,
|
||||
zaps: mentionStat?.zaps || 0,
|
||||
score: mentionStat?.score || 0,
|
||||
score24h: mentionStat?.score24h || 0,
|
||||
satszapped: mentionStat?.satszapped || 0,
|
||||
noteActions,
|
||||
},
|
||||
user: convertToUser(page.users[m.pubkey] || emptyUser(m.pubkey)),
|
||||
mentionedUsers,
|
||||
pubkey: m.pubkey,
|
||||
id: m.id,
|
||||
noteId: nip19.noteEncode(m.id),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1168,6 +1168,11 @@ export const profile = {
|
||||
description: 'Label indicating when the profile joined Nostr (oldest event)',
|
||||
},
|
||||
stats: {
|
||||
gallery: {
|
||||
id: 'profile.gallery',
|
||||
defaultMessage: 'Gallery',
|
||||
description: 'Label for gallery profile stat',
|
||||
},
|
||||
follow: {
|
||||
id: 'profile.followStats',
|
||||
defaultMessage: 'Following',
|
||||
|
Loading…
Reference in New Issue
Block a user