mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Refactor thread route
This commit is contained in:
parent
3bbb03c401
commit
2a7476f979
@ -43,7 +43,6 @@ const Moderation = lazy(() => import('./pages/Settings/Moderation'));
|
||||
const Menu = lazy(() => import('./pages/Settings/Menu'));
|
||||
const Landing = lazy(() => import('./pages/Landing'));
|
||||
const AppDownloadQr = lazy(() => import('./pages/appDownloadQr'));
|
||||
const Longform = lazy(() => import('./pages/Longform'));
|
||||
|
||||
const Terms = lazy(() => import('./pages/Terms'));
|
||||
const Privacy = lazy(() => import('./pages/Privacy'));
|
||||
@ -110,9 +109,8 @@ const Router: Component = () => {
|
||||
<Route path="/" component={Layout} >
|
||||
<Route path="/" component={Landing} />
|
||||
<Route path="/home" component={Home} />
|
||||
<Route path="/thread/:postId" component={Thread} />
|
||||
<Route path="/e/:postId" component={Thread} />
|
||||
<Route path="/l/:naddr" component={Longform} />
|
||||
<Route path="/thread/:id" component={Thread} />
|
||||
<Route path="/e/:id" component={Thread} />
|
||||
<Route path="/explore/:scope?/:timeframe?" component={Explore} />
|
||||
<Route path="/messages/:sender?" component={Messages} />
|
||||
<Route path="/notifications" component={Notifications} />
|
||||
|
@ -334,97 +334,97 @@
|
||||
}
|
||||
}
|
||||
|
||||
.zapHighlights {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
|
||||
&.onlyFew {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.break {
|
||||
flex-basis: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.topZap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-left: 2px;
|
||||
padding-right: 10px;
|
||||
padding-block: 2px;
|
||||
margin: 0;
|
||||
border-radius: 12px;
|
||||
background: var(--devider);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary-2);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--subtile-devider);
|
||||
}
|
||||
|
||||
transition: all 0.6s;
|
||||
}
|
||||
|
||||
.moreZaps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--devider);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
.contextIcon {
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
background-color: var(--text-secondary-2);
|
||||
-webkit-mask: url(../../assets/icons/context.svg) no-repeat 0 / 100%;
|
||||
mask: url(../../assets/icons/context.svg) no-repeat 0 / 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.contextIcon {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.zapHighlights {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
|
||||
&.onlyFew {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.break {
|
||||
flex-basis: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.topZap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-left: 2px;
|
||||
padding-right: 10px;
|
||||
padding-block: 2px;
|
||||
margin: 0;
|
||||
border-radius: 12px;
|
||||
background: var(--devider);
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
.amount {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-secondary-2);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--subtile-devider);
|
||||
}
|
||||
|
||||
transition: all 0.6s;
|
||||
}
|
||||
|
||||
.moreZaps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--devider);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
.contextIcon {
|
||||
width: 16px;
|
||||
height: 14px;
|
||||
background-color: var(--text-secondary-2);
|
||||
-webkit-mask: url(../../assets/icons/context.svg) no-repeat 0 / 100%;
|
||||
mask: url(../../assets/icons/context.svg) no-repeat 0 / 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.contextIcon {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zapHighlightsCompact {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -47,22 +47,22 @@ const NoteTopZaps: Component<{
|
||||
return (
|
||||
|
||||
<Show
|
||||
when={!threadContext?.isFetchingTopZaps}
|
||||
fallback={
|
||||
<div class={styles.topZapsLoading}>
|
||||
<div class={styles.firstZap}></div>
|
||||
<div class={styles.topZaps}>
|
||||
<div class={styles.zapList}>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
when={!threadContext?.isFetchingTopZaps}
|
||||
fallback={
|
||||
<div class={styles.topZapsLoading}>
|
||||
<div class={styles.firstZap}></div>
|
||||
<div class={styles.topZaps}>
|
||||
<div class={styles.zapList}>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
<div class={styles.topZap}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
}
|
||||
>
|
||||
<div class={`${styles.zapHighlights}`}>
|
||||
<TransitionGroup
|
||||
name="top-zaps"
|
||||
|
@ -152,6 +152,23 @@ export const getThread = (user_pubkey: string | undefined, postId: string, subid
|
||||
]));
|
||||
}
|
||||
|
||||
export const getArticleThread = (user_pubkey: string | undefined, pubkey: string, identifier: string, kind: number, subid: string, until = 0, limit = 100) => {
|
||||
|
||||
|
||||
let payload: { user_pubkey?: string, limit: number, pubkey: string, kind: number, identifier: string, until?: number } =
|
||||
{ pubkey, identifier, kind , limit } ;
|
||||
|
||||
if (user_pubkey) {
|
||||
payload.user_pubkey = user_pubkey;
|
||||
}
|
||||
|
||||
sendMessage(JSON.stringify([
|
||||
"REQ",
|
||||
subid,
|
||||
{cache: ["long_form_content_thread_view", payload]},
|
||||
]));
|
||||
}
|
||||
|
||||
export const getFutureExploreFeed = (
|
||||
user_pubkey: string | undefined,
|
||||
subid: string,
|
||||
|
@ -41,11 +41,23 @@
|
||||
}
|
||||
|
||||
.summary {
|
||||
color: var(--text-secondary);
|
||||
font-family: Lora;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
.border {
|
||||
display: block;
|
||||
min-width: 4px;
|
||||
border-radius: 2px;
|
||||
background-color: var(--subtile-devider);
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--text-primary);
|
||||
font-family: Lora;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
|
@ -28,6 +28,7 @@ import NoteTopZaps from "../components/Note/NoteTopZaps";
|
||||
import { parseBolt11 } from "../utils";
|
||||
import { NoteReactionsState } from "../components/Note/Note";
|
||||
import NoteFooter from "../components/Note/NoteFooter/NoteFooter";
|
||||
import { getArticleThread, getThread } from "../lib/feed";
|
||||
|
||||
export type LongFormData = {
|
||||
title: string,
|
||||
@ -288,7 +289,7 @@ It converts "HTML", but keep intact partial entries like "xxxHTMLyyy" and so on.
|
||||
|
||||
`;
|
||||
|
||||
const Longform: Component = () => {
|
||||
const Longform: Component< { naddr: string } > = (props) => {
|
||||
const account = useAccountContext();
|
||||
const params = useParams();
|
||||
const intl = useIntl();
|
||||
@ -300,6 +301,8 @@ const Longform: Component = () => {
|
||||
// @ts-ignore
|
||||
const [author, setAuthor] = createStore<PrimalUser>();
|
||||
|
||||
const naddr = () => props.naddr;
|
||||
|
||||
const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
|
||||
likes: 0,
|
||||
liked: false,
|
||||
@ -327,8 +330,7 @@ const Longform: Component = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const naddr = params.naddr;
|
||||
const subId = `author_${naddr}_${APP_ID}`;
|
||||
const subId = `author_${naddr()}_${APP_ID}`;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, subId, content) =>{
|
||||
if (type === 'EOSE') {
|
||||
@ -355,9 +357,7 @@ const Longform: Component = () => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const naddr = params.naddr;
|
||||
|
||||
if (naddr === 'test') {
|
||||
if (naddr() === 'test') {
|
||||
|
||||
setNote(() => ({
|
||||
title: 'Test Long-Form Note',
|
||||
@ -375,12 +375,12 @@ const Longform: Component = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof naddr === 'string' && naddr.startsWith('naddr')) {
|
||||
const decoded = decodeIdentifier(naddr);
|
||||
if (typeof naddr() === 'string' && naddr().startsWith('naddr')) {
|
||||
const decoded = decodeIdentifier(naddr());
|
||||
|
||||
const { pubkey, identifier, kind } = decoded.data;
|
||||
|
||||
const subId = `naddr_${naddr}_${APP_ID}`;
|
||||
const subId = `naddr_${naddr()}_${APP_ID}`;
|
||||
|
||||
const unsub = subscribeTo(subId, (type, subId, content) =>{
|
||||
if (type === 'EOSE') {
|
||||
@ -405,7 +405,7 @@ const Longform: Component = () => {
|
||||
published: content.created_at || 0,
|
||||
content: content.content,
|
||||
author: content.pubkey,
|
||||
topZaps: [],
|
||||
topZaps: note.topZaps || [],
|
||||
}
|
||||
|
||||
content.tags.forEach(tag => {
|
||||
@ -473,8 +473,8 @@ const Longform: Component = () => {
|
||||
|
||||
const oldZaps = note.topZaps;
|
||||
|
||||
if (oldZaps === undefined) {
|
||||
setNote('topZaps', () => [{ ...zap }]);
|
||||
if (!oldZaps || oldZaps.length === 0) {
|
||||
setNote((n) => ({ ...n, topZaps: [{ ...zap }]}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -484,14 +484,15 @@ const Longform: Component = () => {
|
||||
|
||||
const newZaps = [ ...oldZaps, { ...zap }].sort((a, b) => b.amount - a.amount);
|
||||
|
||||
setNote('topZaps', () => [ ...newZaps ]);
|
||||
setNote((n) => ({ ...n, topZaps: [...newZaps]}));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
getParametrizedEvent(pubkey, identifier, kind, subId);
|
||||
// getThread(account?.publicKey, naddr, subId)
|
||||
getArticleThread(account?.publicKey, pubkey, identifier, kind, subId);
|
||||
}
|
||||
})
|
||||
|
||||
@ -519,20 +520,13 @@ const Longform: Component = () => {
|
||||
{note.title}
|
||||
</div>
|
||||
|
||||
<div class={styles.summary}>
|
||||
{note.summary}
|
||||
</div>
|
||||
|
||||
<img class={styles.image} src={note.image} />
|
||||
|
||||
<div class={styles.tags}>
|
||||
<For each={note.tags}>
|
||||
{tag => (
|
||||
<div class={styles.tag}>
|
||||
{tag}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class={styles.summary}>
|
||||
<div class={styles.border}></div>
|
||||
<div class={styles.text}>
|
||||
{note.summary}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoteTopZaps
|
||||
@ -543,6 +537,15 @@ const Longform: Component = () => {
|
||||
|
||||
<PrimalMarkdown content={note.content || ''} readonly={true} />
|
||||
|
||||
<div class={styles.tags}>
|
||||
<For each={note.tags}>
|
||||
{tag => (
|
||||
<div class={styles.tag}>
|
||||
{tag}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{/* <div class={styles.content} innerHTML={inner()}>
|
||||
<SolidMarkdown
|
||||
children={note.content || ''}
|
||||
|
238
src/pages/NoteThread.tsx
Normal file
238
src/pages/NoteThread.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
import { Component, createEffect, createMemo, For, onCleanup, onMount, Show } from 'solid-js';
|
||||
import Note from '../components/Note/Note';
|
||||
import styles from './NoteThread.module.scss';
|
||||
import { useNavigate, useParams } from '@solidjs/router';
|
||||
import { PrimalNote, PrimalUser, SendNoteResult } from '../types/primal';
|
||||
import PeopleList from '../components/PeopleList/PeopleList';
|
||||
import ReplyToNote from '../components/ReplyToNote/ReplyToNote';
|
||||
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useThreadContext } from '../contexts/ThreadContext';
|
||||
import Wormhole from '../components/Wormhole/Wormhole';
|
||||
import { useAccountContext } from '../contexts/AccountContext';
|
||||
import { sortByRecency } from '../stores/note';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import Search from '../components/Search/Search';
|
||||
import { placeholders as tPlaceholders, thread as t } from '../translations';
|
||||
import { userName } from '../stores/profile';
|
||||
import PageTitle from '../components/PageTitle/PageTitle';
|
||||
import NavHeader from '../components/NavHeader/NavHeader';
|
||||
import Loader from '../components/Loader/Loader';
|
||||
import { isIOS } from '../components/BannerIOS/BannerIOS';
|
||||
import { unwrap } from 'solid-js/store';
|
||||
|
||||
|
||||
const NoteThread: Component<{ noteId: string }> = (props) => {
|
||||
const account = useAccountContext();
|
||||
const params = useParams();
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
let repliesHolder: HTMLDivElement | undefined;
|
||||
|
||||
let initialPostId = '';
|
||||
|
||||
const postId = () => {
|
||||
const { noteId } = props;
|
||||
|
||||
if (noteId.startsWith('note')) {
|
||||
return noteId;
|
||||
}
|
||||
|
||||
if (noteId.startsWith('nevent')) {
|
||||
return nip19.noteEncode(nip19.decode(noteId).data.id);
|
||||
}
|
||||
|
||||
return nip19.noteEncode(noteId);
|
||||
};
|
||||
|
||||
const threadContext = useThreadContext();
|
||||
|
||||
const primaryNote = createMemo(() => {
|
||||
|
||||
let note = threadContext?.notes.find(n => n.post.noteId === postId());
|
||||
|
||||
// Return the note if found
|
||||
if (note) {
|
||||
return note;
|
||||
}
|
||||
|
||||
// Since there is no note see if this is a repost
|
||||
note = threadContext?.notes.find(n => n.repost?.note.noteId === postId());
|
||||
|
||||
// If reposted note found redirect to it's thread
|
||||
note && navigate(`/e/${note?.post.noteId}`)
|
||||
|
||||
return note;
|
||||
});
|
||||
|
||||
const parentNotes = () => {
|
||||
const note = primaryNote();
|
||||
|
||||
if (!note) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sortByRecency(
|
||||
threadContext?.notes.filter(n =>
|
||||
n.post.id !== note.post.id && n.post.created_at <= note.post.created_at,
|
||||
) || [],
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
const replyNotes = () => {
|
||||
const note = primaryNote();
|
||||
|
||||
if (!note) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return threadContext?.notes.filter(n =>
|
||||
n.post.id !== note.post.id && n.post.created_at >= note.post.created_at,
|
||||
) || [];
|
||||
};
|
||||
|
||||
const people = () => {
|
||||
const authors = (threadContext?.notes || []).
|
||||
reduce<PrimalUser[]>((acc, n) => acc.find(u => u.pubkey === n.user.pubkey) ? [...acc] : [ ...acc, { ...n.user }], []);
|
||||
|
||||
const mentions = Object.values(primaryNote()?.mentionedUsers || {}).
|
||||
filter((u) => !authors.find(a => u.pubkey === a.pubkey));
|
||||
|
||||
return [ ...authors, ...mentions ];
|
||||
};
|
||||
|
||||
const isFetching = () => threadContext?.isFetching;
|
||||
|
||||
createEffect(() => {
|
||||
const pid = postId();
|
||||
|
||||
if (pid !== initialPostId) {
|
||||
threadContext?.actions.fetchNotes(pid);
|
||||
initialPostId = pid;
|
||||
}
|
||||
});
|
||||
|
||||
let observer: IntersectionObserver | undefined;
|
||||
|
||||
createEffect(() => {
|
||||
if (!primaryNote() || threadContext?.isFetching) return;
|
||||
|
||||
const pn = document.getElementById('primary_note');
|
||||
|
||||
if (!pn) return;
|
||||
|
||||
setTimeout(() => {
|
||||
const threadHeader = 80;
|
||||
const iOSBanner = 54;
|
||||
|
||||
const rect = pn.getBoundingClientRect();
|
||||
const wh = window.innerHeight - threadHeader;
|
||||
|
||||
const block = rect.height < wh && parentNotes().length > 0 ?
|
||||
'end' : 'start';
|
||||
|
||||
pn.scrollIntoView({ block });
|
||||
|
||||
if (block === 'start') {
|
||||
const moreScroll = threadHeader + (isIOS() ? iOSBanner : 0);
|
||||
window.scrollBy({ top: -moreScroll });
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
const pn = document.getElementById('primary_note');
|
||||
|
||||
pn && observer?.unobserve(pn);
|
||||
});
|
||||
|
||||
const onNotePosted = (result: SendNoteResult) => {
|
||||
threadContext?.actions.fetchNotes(postId());
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title={
|
||||
intl.formatMessage(
|
||||
t.pageTitle,
|
||||
{ name: userName(primaryNote()?.user) },
|
||||
)}
|
||||
/>
|
||||
|
||||
<Wormhole
|
||||
to="search_section"
|
||||
>
|
||||
<Search />
|
||||
</Wormhole>
|
||||
|
||||
<Wormhole to='right_sidebar'>
|
||||
<PeopleList
|
||||
note={primaryNote()}
|
||||
people={people()}
|
||||
label={intl.formatMessage(t.sidebar)}
|
||||
mentionLabel={intl.formatMessage(t.sidebarMentions)}
|
||||
/>
|
||||
</Wormhole>
|
||||
|
||||
<NavHeader title="Thread" />
|
||||
|
||||
<Show when={account?.isKeyLookupDone}>
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={<Loader />}
|
||||
>
|
||||
<div class={styles.parentsHolder}>
|
||||
<For each={parentNotes()}>
|
||||
{note =>
|
||||
<div>
|
||||
<Note note={note} parent={true} shorten={true} />
|
||||
</div>
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={primaryNote()}
|
||||
fallback={
|
||||
<div class={styles.missingNote}>
|
||||
<p>
|
||||
{intl.formatMessage(tPlaceholders.missingNote.firstLine)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(tPlaceholders.missingNote.secondLine)}
|
||||
</p>
|
||||
</div>
|
||||
}>
|
||||
<div id="primary_note">
|
||||
<Note
|
||||
note={primaryNote() as PrimalNote}
|
||||
noteType="primary"
|
||||
quoteCount={threadContext?.quoteCount}
|
||||
/>
|
||||
<Show when={account?.hasPublicKey()}>
|
||||
<ReplyToNote
|
||||
note={primaryNote() as PrimalNote}
|
||||
onNotePosted={onNotePosted}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={styles.repliesHolder} ref={repliesHolder}>
|
||||
<For each={replyNotes()}>
|
||||
{note =>
|
||||
<div>
|
||||
<Note note={note} shorten={true} />
|
||||
</div>
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteThread;
|
@ -1,236 +1,60 @@
|
||||
import { Component, createEffect, createMemo, For, onCleanup, onMount, Show } from 'solid-js';
|
||||
import Note from '../components/Note/Note';
|
||||
import styles from './Thread.module.scss';
|
||||
import { useNavigate, useParams } from '@solidjs/router';
|
||||
import { PrimalNote, PrimalUser, SendNoteResult } from '../types/primal';
|
||||
import PeopleList from '../components/PeopleList/PeopleList';
|
||||
import ReplyToNote from '../components/ReplyToNote/ReplyToNote';
|
||||
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useThreadContext } from '../contexts/ThreadContext';
|
||||
import { Component, onMount } from 'solid-js';
|
||||
import Branding from '../components/Branding/Branding';
|
||||
import Wormhole from '../components/Wormhole/Wormhole';
|
||||
import { useAccountContext } from '../contexts/AccountContext';
|
||||
import { sortByRecency } from '../stores/note';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import Search from '../components/Search/Search';
|
||||
import { placeholders as tPlaceholders, thread as t } from '../translations';
|
||||
import { userName } from '../stores/profile';
|
||||
|
||||
import appstoreImg from '../assets/images/appstore_download.svg';
|
||||
import playstoreImg from '../assets/images/playstore_download.svg';
|
||||
|
||||
import gitHubLight from '../assets/icons/github_light.svg';
|
||||
import gitHubDark from '../assets/icons/github.svg';
|
||||
|
||||
import primalDownloads from '../assets/images/primal_downloads.png';
|
||||
|
||||
import styles from './Downloads.module.scss';
|
||||
import { downloads as t } from '../translations';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import StickySidebar from '../components/StickySidebar/StickySidebar';
|
||||
import { appStoreLink, playstoreLink, apkLink } from '../constants';
|
||||
import ExternalLink from '../components/ExternalLink/ExternalLink';
|
||||
import PageCaption from '../components/PageCaption/PageCaption';
|
||||
import PageTitle from '../components/PageTitle/PageTitle';
|
||||
import NavHeader from '../components/NavHeader/NavHeader';
|
||||
import Loader from '../components/Loader/Loader';
|
||||
import { isIOS } from '../components/BannerIOS/BannerIOS';
|
||||
import { unwrap } from 'solid-js/store';
|
||||
import { useSettingsContext } from '../contexts/SettingsContext';
|
||||
import { useParams } from '@solidjs/router';
|
||||
import NotFound from './NotFound';
|
||||
import NoteThread from './NoteThread';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import Longform from './Longform';
|
||||
|
||||
const EventPage: Component = () => {
|
||||
|
||||
const Thread: Component = () => {
|
||||
const account = useAccountContext();
|
||||
const params = useParams();
|
||||
const intl = useIntl();
|
||||
const navigate = useNavigate();
|
||||
|
||||
let repliesHolder: HTMLDivElement | undefined;
|
||||
const render = () => {
|
||||
const { id } = params;
|
||||
|
||||
let initialPostId = '';
|
||||
if (!id) return <NotFound />;
|
||||
|
||||
const postId = () => {
|
||||
if (params.postId.startsWith('note')) {
|
||||
return params.postId;
|
||||
if (id.startsWith('naddr1')) {
|
||||
return <Longform naddr={id} />
|
||||
}
|
||||
|
||||
if (params.postId.startsWith('nevent')) {
|
||||
return nip19.noteEncode(nip19.decode(params.postId).data.id);
|
||||
if (id.startsWith('note1')) {
|
||||
return <NoteThread noteId={id} />
|
||||
}
|
||||
|
||||
return nip19.noteEncode(params.postId);
|
||||
if (id.startsWith('nevent1')) {
|
||||
const noteId = nip19.noteEncode(nip19.decode(id).data.id);
|
||||
|
||||
return <NoteThread noteId={noteId} />
|
||||
}
|
||||
|
||||
const noteId = nip19.noteEncode(id);
|
||||
|
||||
return <NoteThread noteId={noteId} />
|
||||
};
|
||||
|
||||
const threadContext = useThreadContext();
|
||||
|
||||
const primaryNote = createMemo(() => {
|
||||
|
||||
let note = threadContext?.notes.find(n => n.post.noteId === postId());
|
||||
|
||||
// Return the note if found
|
||||
if (note) {
|
||||
return note;
|
||||
}
|
||||
|
||||
// Since there is no note see if this is a repost
|
||||
note = threadContext?.notes.find(n => n.repost?.note.noteId === postId());
|
||||
|
||||
// If reposted note found redirect to it's thread
|
||||
note && navigate(`/e/${note?.post.noteId}`)
|
||||
|
||||
return note;
|
||||
});
|
||||
|
||||
const parentNotes = () => {
|
||||
const note = primaryNote();
|
||||
|
||||
if (!note) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sortByRecency(
|
||||
threadContext?.notes.filter(n =>
|
||||
n.post.id !== note.post.id && n.post.created_at <= note.post.created_at,
|
||||
) || [],
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
const replyNotes = () => {
|
||||
const note = primaryNote();
|
||||
|
||||
if (!note) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return threadContext?.notes.filter(n =>
|
||||
n.post.id !== note.post.id && n.post.created_at >= note.post.created_at,
|
||||
) || [];
|
||||
};
|
||||
|
||||
const people = () => {
|
||||
const authors = (threadContext?.notes || []).
|
||||
reduce<PrimalUser[]>((acc, n) => acc.find(u => u.pubkey === n.user.pubkey) ? [...acc] : [ ...acc, { ...n.user }], []);
|
||||
|
||||
const mentions = Object.values(primaryNote()?.mentionedUsers || {}).
|
||||
filter((u) => !authors.find(a => u.pubkey === a.pubkey));
|
||||
|
||||
return [ ...authors, ...mentions ];
|
||||
};
|
||||
|
||||
const isFetching = () => threadContext?.isFetching;
|
||||
|
||||
createEffect(() => {
|
||||
const pid = postId();
|
||||
|
||||
if (pid !== initialPostId) {
|
||||
threadContext?.actions.fetchNotes(pid);
|
||||
initialPostId = pid;
|
||||
}
|
||||
});
|
||||
|
||||
let observer: IntersectionObserver | undefined;
|
||||
|
||||
createEffect(() => {
|
||||
if (!primaryNote() || threadContext?.isFetching) return;
|
||||
|
||||
const pn = document.getElementById('primary_note');
|
||||
|
||||
if (!pn) return;
|
||||
|
||||
setTimeout(() => {
|
||||
const threadHeader = 80;
|
||||
const iOSBanner = 54;
|
||||
|
||||
const rect = pn.getBoundingClientRect();
|
||||
const wh = window.innerHeight - threadHeader;
|
||||
|
||||
const block = rect.height < wh && parentNotes().length > 0 ?
|
||||
'end' : 'start';
|
||||
|
||||
pn.scrollIntoView({ block });
|
||||
|
||||
if (block === 'start') {
|
||||
const moreScroll = threadHeader + (isIOS() ? iOSBanner : 0);
|
||||
window.scrollBy({ top: -moreScroll });
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
const pn = document.getElementById('primary_note');
|
||||
|
||||
pn && observer?.unobserve(pn);
|
||||
});
|
||||
|
||||
const onNotePosted = (result: SendNoteResult) => {
|
||||
threadContext?.actions.fetchNotes(postId());
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title={
|
||||
intl.formatMessage(
|
||||
t.pageTitle,
|
||||
{ name: userName(primaryNote()?.user) },
|
||||
)}
|
||||
/>
|
||||
|
||||
<Wormhole
|
||||
to="search_section"
|
||||
>
|
||||
<Search />
|
||||
</Wormhole>
|
||||
|
||||
<Wormhole to='right_sidebar'>
|
||||
<PeopleList
|
||||
note={primaryNote()}
|
||||
people={people()}
|
||||
label={intl.formatMessage(t.sidebar)}
|
||||
mentionLabel={intl.formatMessage(t.sidebarMentions)}
|
||||
/>
|
||||
</Wormhole>
|
||||
|
||||
<NavHeader title="Thread" />
|
||||
|
||||
<Show when={account?.isKeyLookupDone}>
|
||||
<Show
|
||||
when={!isFetching()}
|
||||
fallback={<Loader />}
|
||||
>
|
||||
<div class={styles.parentsHolder}>
|
||||
<For each={parentNotes()}>
|
||||
{note =>
|
||||
<div>
|
||||
<Note note={note} parent={true} shorten={true} />
|
||||
</div>
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={primaryNote()}
|
||||
fallback={
|
||||
<div class={styles.missingNote}>
|
||||
<p>
|
||||
{intl.formatMessage(tPlaceholders.missingNote.firstLine)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(tPlaceholders.missingNote.secondLine)}
|
||||
</p>
|
||||
</div>
|
||||
}>
|
||||
<div id="primary_note">
|
||||
<Note
|
||||
note={primaryNote() as PrimalNote}
|
||||
noteType="primary"
|
||||
quoteCount={threadContext?.quoteCount}
|
||||
/>
|
||||
<Show when={account?.hasPublicKey()}>
|
||||
<ReplyToNote
|
||||
note={primaryNote() as PrimalNote}
|
||||
onNotePosted={onNotePosted}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={styles.repliesHolder} ref={repliesHolder}>
|
||||
<For each={replyNotes()}>
|
||||
{note =>
|
||||
<div>
|
||||
<Note note={note} shorten={true} />
|
||||
</div>
|
||||
}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
return <>{render()}</>;
|
||||
}
|
||||
|
||||
export default Thread;
|
||||
export default EventPage;
|
||||
|
Loading…
Reference in New Issue
Block a user