From 2a7476f979943a30b467a104c5bac3f8418b3e5f Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Thu, 23 May 2024 16:36:56 +0200 Subject: [PATCH] Refactor thread route --- src/Router.tsx | 6 +- src/components/Note/Note.module.scss | 174 ++++++------ src/components/Note/NoteTopZaps.tsx | 28 +- src/lib/feed.ts | 17 ++ src/pages/Longform.module.scss | 22 +- src/pages/Longform.tsx | 55 ++-- ...ead.module.scss => NoteThread.module.scss} | 0 src/pages/NoteThread.tsx | 238 ++++++++++++++++ src/pages/Thread.tsx | 262 +++--------------- 9 files changed, 447 insertions(+), 355 deletions(-) rename src/pages/{Thread.module.scss => NoteThread.module.scss} (100%) create mode 100644 src/pages/NoteThread.tsx diff --git a/src/Router.tsx b/src/Router.tsx index 03ffc30..be33e78 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -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 = () => { - - - + + diff --git a/src/components/Note/Note.module.scss b/src/components/Note/Note.module.scss index ad190ce..855c917 100644 --- a/src/components/Note/Note.module.scss +++ b/src/components/Note/Note.module.scss @@ -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; diff --git a/src/components/Note/NoteTopZaps.tsx b/src/components/Note/NoteTopZaps.tsx index ce7c7bb..348ca33 100644 --- a/src/components/Note/NoteTopZaps.tsx +++ b/src/components/Note/NoteTopZaps.tsx @@ -47,22 +47,22 @@ const NoteTopZaps: Component<{ return ( -
-
-
-
-
-
-
-
+ when={!threadContext?.isFetchingTopZaps} + fallback={ +
+
+
+
+
+
+
+
+
+
-
- } - > + } + >
{ + + + 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, diff --git a/src/pages/Longform.module.scss b/src/pages/Longform.module.scss index 46f1c80..2b8d448 100644 --- a/src/pages/Longform.module.scss +++ b/src/pages/Longform.module.scss @@ -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 { diff --git a/src/pages/Longform.tsx b/src/pages/Longform.tsx index b1e18ba..f7b1f9f 100644 --- a/src/pages/Longform.tsx +++ b/src/pages/Longform.tsx @@ -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(); + const naddr = () => props.naddr; + const [reactionsState, updateReactionsState] = createStore({ 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}
-
- {note.summary} -
- -
- - {tag => ( -
- {tag} -
- )} -
+
+
+
+ {note.summary} +
{ +
+ + {tag => ( +
+ {tag} +
+ )} +
+
{/*
= (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((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 ( +
+ + + + + + + + + + + + + + } + > +
+ + {note => +
+ +
+ } +
+
+ + +

+ {intl.formatMessage(tPlaceholders.missingNote.firstLine)} +

+

+ {intl.formatMessage(tPlaceholders.missingNote.secondLine)} +

+
+ }> +
+ + + + +
+ + +
+ + {note => +
+ +
+ } +
+
+ + +
+ ) +} + +export default NoteThread; diff --git a/src/pages/Thread.tsx b/src/pages/Thread.tsx index ecdd796..61e199b 100644 --- a/src/pages/Thread.tsx +++ b/src/pages/Thread.tsx @@ -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 ; - const postId = () => { - if (params.postId.startsWith('note')) { - return params.postId; + if (id.startsWith('naddr1')) { + return } - if (params.postId.startsWith('nevent')) { - return nip19.noteEncode(nip19.decode(params.postId).data.id); + if (id.startsWith('note1')) { + return } - return nip19.noteEncode(params.postId); + if (id.startsWith('nevent1')) { + const noteId = nip19.noteEncode(nip19.decode(id).data.id); + + return + } + + const noteId = nip19.noteEncode(id); + + return }; - 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((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 ( -
- - - - - - - - - - - - - - } - > -
- - {note => -
- -
- } -
-
- - -

- {intl.formatMessage(tPlaceholders.missingNote.firstLine)} -

-

- {intl.formatMessage(tPlaceholders.missingNote.secondLine)} -

-
- }> -
- - - - -
- - -
- - {note => -
- -
- } -
-
- - -
- ) + return <>{render()}; } -export default Thread; +export default EventPage;