From c88cfa74bbc5a78703663cf01de5e1430ee4fca1 Mon Sep 17 00:00:00 2001 From: OFF0 Date: Sat, 15 Apr 2023 15:33:44 +0200 Subject: [PATCH] refactor: improve view and move code to ui and notes cleanup code and move parts to ui.ts and notes.ts. simplify view and fix some weird animation issue, it should run pretty stable now. updated color and spacings. profile view now showing kind 0 name, but it is unnecessarily re-rendering. this part should probably go to a custom profil subscription callback in the future. keeping as is for now and refactor later. --- src/main.ts | 168 +++++++++++++++---------------------------- src/notes.ts | 16 +++++ src/profiles.ts | 25 ++++++- src/styles/cards.css | 18 ++--- src/styles/main.css | 18 +++-- src/styles/view.css | 36 ++++++++-- src/ui.ts | 84 ++++++++++++++++++++++ src/view.ts | 74 ++++++++++++------- 8 files changed, 279 insertions(+), 160 deletions(-) create mode 100644 src/notes.ts create mode 100644 src/ui.ts diff --git a/src/main.ts b/src/main.ts index 47ddd94..487b5f4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,88 +1,25 @@ import {Event, nip19} from 'nostr-tools'; import {zeroLeadingBitsCount} from './utils/crypto'; -import {elem, elemArticle, parseTextContent} from './utils/dom'; -import {bounce, dateTime, formatTime} from './utils/time'; +import {elem} from './utils/dom'; +import {bounce} from './utils/time'; import {isWssUrl} from './utils/url'; import {sub24hFeed, subNote, subProfile} from './subscriptions' -import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt, validatePow} from './events'; -import {clearView, getViewContent, getViewElem, setViewElem, view} from './view'; +import {getReplyTo, hasEventTag, isMention, sortByCreatedAt, sortEventCreatedAt} from './events'; +import {clearView, getViewContent, getViewElem, getViewOptions, setViewElem, view} from './view'; import {closeSettingsView, config, toggleSettingsView} from './settings'; -import {getReactions, getReactionContents, handleReaction, handleUpvote} from './reactions'; +import {handleReaction, handleUpvote} from './reactions'; import {closePublishView, openWriteInput, togglePublishView} from './write'; -import {linkPreview} from './media'; -import {getMetadata, handleMetadata} from './profiles'; +import {handleMetadata, renderProfile} from './profiles'; +import {EventWithNip19, EventWithNip19AndReplyTo, textNoteList, replyList} from './notes'; +import {createTextNote, renderRecommendServer} from './ui'; // curl -H 'accept: application/nostr+json' https://relay.nostr.ch/ -type EventWithNip19 = Event & { - nip19: { - note: string; - npub: string; - } -}; -const textNoteList: Array = []; // could use indexDB - type EventRelayMap = { [eventId: string]: string[]; }; const eventRelayMap: EventRelayMap = {}; // eventId: [relay1, relay2] -type EventWithNip19AndReplyTo = EventWithNip19 & { - replyTo: string; -}; - -const replyList: Array = []; - -const createTextNote = (evt: EventWithNip19, relay: string) => { - const {host, img, name, time, userName} = getMetadata(evt, relay); - const replies = replyList.filter(({replyTo}) => replyTo === evt.id); - // const isLongContent = evt.content.trimRight().length > 280; - // const content = isLongContent ? evt.content.slice(0, 280) : evt.content; - const reactions = getReactions(evt.id); - const didReact = reactions.length && !!reactions.find(reaction => reaction.pubkey === config.pubkey); - const replyFeed: Array = replies[0] ? replies.sort(sortByCreatedAt).map(e => setViewElem(e.id, createTextNote(e, relay))) : []; - const [content, {firstLink}] = parseTextContent(evt.content); - const buttons = elem('div', {className: 'buttons'}, [ - elem('button', {name: 'reply', type: 'button'}, [ - elem('img', {height: 24, width: 24, src: '/assets/comment.svg'}) - ]), - elem('button', {name: 'star', type: 'button'}, [ - elem('img', { - alt: didReact ? '✭' : '✩', // ♥ - height: 24, width: 24, - src: `/assets/${didReact ? 'star-fill' : 'star'}.svg`, - title: getReactionContents(evt.id).join(' '), - }), - elem('small', {data: {reactions: ''}}, reactions.length || ''), - ]), - ]); - const body = elem('div', {className: 'mbox-body'}, [ - elem('header', { - className: 'mbox-header', - title: `User: ${userName}\n${time}\n\nUser pubkey: ${evt.pubkey}\n\nRelay: ${host}\n\nEvent-id: ${evt.id} - ${evt.tags.length ? `\nTags ${JSON.stringify(evt.tags)}\n` : ''} - ${evt.content}` - }, [ - elem('a', {className: `mbox-username${name ? ' mbox-kind0-name' : ''}`, href: `/${evt.nip19.npub}`}, name || userName), - ' ', - elem('a', {href: `/${evt.nip19.note}`}, elem('time', {dateTime: time.toISOString()}, formatTime(time))), - ]), - elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [ - ...content, - (firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : null, - ]), - buttons, - ]); - if (localStorage.getItem('reply_to') === evt.id) { - openWriteInput(buttons, evt.id); - } - return elemArticle([ - elem('div', {className: 'mbox-img'}, img), - body, - ...(replies[0] ? [elem('div', {className: 'mobx-replies'}, replyFeed.reverse())] : []), - ], {data: {id: evt.id, pubkey: evt.pubkey, relay}}); -}; - const renderNote = ( evt: EventWithNip19, i: number, @@ -108,18 +45,46 @@ const hasEnoughPOW = ( }; const renderFeed = bounce(() => { - const now = Math.floor(Date.now() * 0.001); - textNoteList - // dont render notes from the future - .filter(note => note.created_at <= now) - // if difficulty filter is configured dont render notes with too little pow - .filter(note => !config.filterDifficulty || note.tags.some(tag => hasEnoughPOW(tag, note.id))) - .sort(sortByCreatedAt) - .reverse() - .forEach(renderNote); + const view = getViewOptions(); + switch (view.type) { + case 'note': + textNoteList + .concat(replyList) + .filter(note => note.id === view.id) + .forEach(renderNote); + break; + case 'profile': + const isEvent = (evt?: T): evt is T => evt !== undefined; + [ + ...textNoteList + .filter(note => note.pubkey === view.id), + ...replyList.filter(reply => reply.pubkey === view.id) + .map(reply => textNoteList.find(note => note.id === reply.replyTo) || replyList.find(note => note.id === reply.replyTo) ) + .filter(isEvent) + ] + .sort(sortByCreatedAt) + .reverse() + .forEach(renderNote); // render in-reply-to + + renderProfile(view.id); + break; + case 'feed': + const now = Math.floor(Date.now() * 0.001); + textNoteList + .filter(note => { + // dont render notes from the future + if (note.created_at > now) return false; + // if difficulty filter is configured dont render notes with too little pow + return !config.filterDifficulty || note.tags.some(tag => hasEnoughPOW(tag, note.id)) + }) + .sort(sortByCreatedAt) + .reverse() + .forEach(renderNote); + break; + } }, 17); // (16.666 rounded, an arbitrary value to limit updates to max 60x per s) -const renderReply = (evt: EventWithNip19AndReplyTo, relay: string) => { +const renderReply = (evt: EventWithNip19AndReplyTo) => { const parent = getViewElem(evt.replyTo); if (!parent) { // root article has not been rendered return; @@ -129,7 +94,7 @@ const renderReply = (evt: EventWithNip19AndReplyTo, relay: string) => { replyContainer = elem('div', {className: 'mobx-replies'}); parent.append(replyContainer); } - const reply = createTextNote(evt, relay); + const reply = createTextNote(evt, eventRelayMap[evt.id][0]); replyContainer.append(reply); setViewElem(evt.id, reply); }; @@ -148,7 +113,7 @@ const handleReply = (evt: EventWithNip19, relay: string) => { } const evtWithReplyTo = {replyTo, ...evt}; replyList.push(evtWithReplyTo); - renderReply(evtWithReplyTo, relay); + renderReply(evtWithReplyTo); }; const handleTextNote = (evt: Event, relay: string) => { @@ -183,27 +148,6 @@ config.rerenderFeed = () => { renderFeed(); }; -setInterval(() => { - document.querySelectorAll('time[datetime]').forEach((timeElem: HTMLTimeElement) => { - timeElem.textContent = formatTime(new Date(timeElem.dateTime)); - }); -}, 10000); - -const renderRecommendServer = (evt: Event, relay: string) => { - const {img, name, time, userName} = getMetadata(evt, relay); - const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [ - elem('header', {className: 'mbox-header'}, [ - elem('small', {}, [ - elem('strong', {}, userName) - ]), - ]), - ` recommends server: ${evt.content}`, - ]); - return elemArticle([ - elem('div', {className: 'mbox-img'}, [img]), body - ], {className: 'mbox-recommend-server', data: {id: evt.id, pubkey: evt.pubkey}}); -}; - const handleRecommendServer = (evt: Event, relay: string) => { if (getViewElem(evt.id) || !isWssUrl(evt.content)) { return; @@ -246,7 +190,7 @@ const onEvent = (evt: Event, relay: string) => { const route = (path: string) => { if (path === '/') { sub24hFeed(onEvent); - view('/'); + view('/', {type: 'feed'}); } else if (path.length === 64 && path.match(/^\/[0-9a-z]+$/)) { const {type, data} = nip19.decode(path.slice(1)); if (typeof data !== 'string') { @@ -256,15 +200,16 @@ const route = (path: string) => { switch(type) { case 'note': subNote(data, onEvent); - view(path); + view(path, {type: 'note', id: data}); break; case 'npub': subProfile(data, onEvent); - view(path); + view(path, {type: 'profile', id: data}); break; default: console.warn(`type ${type} not yet supported`); } + renderFeed(); } }; @@ -273,7 +218,6 @@ route(location.pathname); history.pushState({}, '', location.pathname); window.addEventListener('popstate', (event) => { - // console.log(`popstate: ${location.pathname}, state: ${JSON.stringify(event.state)}`); route(location.pathname); }); @@ -283,13 +227,17 @@ const handleLink = (a: HTMLAnchorElement, e: MouseEvent) => { console.warn('expected anchor to have href attribute', a); return; } + closeSettingsView(); + closePublishView(); + if (href === location.pathname) { + e.preventDefault(); + return; + } if ( href === '/' || href.startsWith('/note') || href.startsWith('/npub') ) { - closeSettingsView(); - closePublishView(); route(href); history.pushState({}, '', href); e.preventDefault(); diff --git a/src/notes.ts b/src/notes.ts new file mode 100644 index 0000000..b94dfce --- /dev/null +++ b/src/notes.ts @@ -0,0 +1,16 @@ +import {Event} from 'nostr-tools'; + +export type EventWithNip19 = Event & { + nip19: { + note: string; + npub: string; + } +}; + +export const textNoteList: Array = []; // could use indexDB + +export type EventWithNip19AndReplyTo = EventWithNip19 & { + replyTo: string; +}; + +export const replyList: Array = []; diff --git a/src/profiles.ts b/src/profiles.ts index ccbe0d9..1c64302 100644 --- a/src/profiles.ts +++ b/src/profiles.ts @@ -1,6 +1,7 @@ import {Event} from 'nostr-tools'; import {elem, elemCanvas} from './utils/dom'; import {getHost, getNoxyUrl} from './utils/url'; +import {getViewContent, getViewElem} from './view'; import {validatePow} from './events'; import {parseContent} from './media'; @@ -66,7 +67,8 @@ const setMetadata = ( const name = user.metadata[relay].name || user.name || ''; if (name) { document.body - .querySelectorAll(`[data-pubkey="${evt.pubkey}"] .mbox-username:not(.mbox-kind0-name)`) + // TODO: this should not depend on specific DOM structure, move pubkey info on username element + .querySelectorAll(`[data-pubkey="${evt.pubkey}"] > .mbox-body > header .mbox-username:not(.mbox-kind0-name)`) .forEach((username: HTMLElement) => { username.textContent = name; username.classList.add('mbox-kind0-name'); @@ -103,9 +105,11 @@ export const handleMetadata = (evt: Event, relay: string) => { setMetadata(evt, relay, metadata); }; +export const getProfile = (pubkey: string) => userList.find(user => user.pubkey === pubkey); + export const getMetadata = (evt: Event, relay: string) => { const host = getHost(relay); - const user = userList.find(user => user.pubkey === evt.pubkey); + const user = getProfile(evt.pubkey); const userImg = user?.picture; const name = user?.metadata[relay]?.name || user?.name; const userName = name || evt.pubkey.slice(0, 8); @@ -156,3 +160,20 @@ export const getMetadata = (evt: Event, relay: string) => { // ]); // return renderArticle([img, body], {className: 'mbox-updated-contact', data: {id: evt.id, pubkey: evt.pubkey, relay}}); // } + +export const renderProfile = (id: string) => { + const content = getViewContent(); + const header = getViewElem(id); + if (!content || !header) { + return; + } + const profile = getProfile(id); + if (profile && profile.name) { + const h1 = header.querySelector('h1'); + if (h1) { + h1.textContent = profile.name; + } else { + header.prepend(elem('h1', {}, profile.name)); + } + } +}; \ No newline at end of file diff --git a/src/styles/cards.css b/src/styles/cards.css index 37e061c..b94c656 100644 --- a/src/styles/cards.css +++ b/src/styles/cards.css @@ -1,8 +1,5 @@ /* https://developer.mozilla.org/en-US/docs/Web/CSS/Layout_cookbook/Media_objects */ .mbox { - --profileimg-size: 4rem; - --profileimg-size-half: 2rem; - --profileimg-size-quarter: 1rem; align-items: center; display: flex; flex-direction: row; @@ -54,9 +51,6 @@ } .mbox-header { - flex-basis: calc(100% - var(--profileimg-size) - var(--gap-half)); - flex-grow: 0; - flex-shrink: 1; margin-top: 0; } .mbox-header a { @@ -121,21 +115,21 @@ display: block; height: 200vh; left: var(--profileimg-size-half); - margin-left: -.2rem; + margin-left: -.1rem; position: absolute; top: -200vh; - width: .4rem; + width: .2rem; } .mobx-replies .mbox .mbox::before { background: none; border-color: var(--bgcolor-inactive);; border-style: solid; - border-width: 0 0 .4rem .4rem; + border-width: 0 0 .2rem .2rem; content: ""; display: block; height: var(--profileimg-size-quarter); left: calc(-1 * var(--profileimg-size-quarter)); - margin-left: -.2rem; + margin-left: -.1rem; position: absolute; top: 0; width: .8rem; @@ -147,10 +141,10 @@ display: block; height: 100vh; left: calc(-1 * var(--profileimg-size-quarter)); - margin-left: -.2rem; + margin-left: -.1rem; position: absolute; top: -100vh; - width: .4rem; + width: .2rem; } /* support visualisation of 3 levels of thread nesting, rest render flat without line */ .mbox .mobx-replies .mobx-replies::before, diff --git a/src/styles/main.css b/src/styles/main.css index 16da13b..bd32aed 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -5,9 +5,10 @@ @import "error.css"; :root { + --content-width: min(100% - 2.4rem, 96ch); /* 5px auto Highlight */ --focus-border-color: rgb(0, 122, 255); - --focus-border-radius: 2px; + --focus-border-radius: .2rem; --focus-outline-color: rgb(192, 227, 252); --focus-outline-offset: 2px; --focus-outline-style: solid; @@ -16,7 +17,9 @@ --font-small: 1.2rem; --gap: 2.4rem; --gap-half: 1.2rem; - --content-width: min(100% - 2.4rem, 96ch); + --profileimg-size: 4rem; + --profileimg-size-half: 2rem; + --profileimg-size-quarter: 1rem; } ::selection { @@ -30,7 +33,8 @@ @media (prefers-color-scheme: light) { html { - --bgcolor: #fdfefa; + --bgcolor: #fff; + --bgcolor-nav: gainsboro; --bgcolor-accent: #7badfc; --bgcolor-danger: rgb(225, 40, 40); --bgcolor-danger-input: rgba(255 255 255 / .85); @@ -45,6 +49,7 @@ @media (prefers-color-scheme: dark) { html { --bgcolor: #191919; + --bgcolor-nav: darkslateblue; --bgcolor-accent: rgb(16, 93, 176); --bgcolor-danger: rgb(169, 0, 0); --bgcolor-danger-input: rgba(0 0 0 / .5); @@ -74,6 +79,7 @@ body { color: var(--color); font-size: 1.6rem; line-height: 1.5; + word-break: break-all; } html, body { @@ -119,9 +125,11 @@ a:focus { outline: var(--focus-outline); outline-offset: 0; } - a:visited { - color: darkmagenta; + color: darkslateblue; +} +nav a:visited { + color: inherit; } img[alt] { diff --git a/src/styles/view.css b/src/styles/view.css index c1d4524..2b2004f 100644 --- a/src/styles/view.css +++ b/src/styles/view.css @@ -20,18 +20,18 @@ main { } aside { - z-index: 2; + z-index: 4; } nav { - background-color: indigo; + background-color: var(--bgcolor-nav); display: flex; flex-direction: row; flex-grow: 1; flex-shrink: 0; - justify-content: space-around; + justify-content: space-between; overflow-y: auto; - padding: 1rem 1.5rem; + padding: 0 1.5rem; user-select: none; -webkit-user-select: none; } @@ -46,6 +46,19 @@ nav { justify-content: space-between; } } +nav a, +nav button { + --bgcolor-accent: transparent; + --border-color: transparent; + border-radius: 0; + padding: 1rem; +} +@media (orientation: landscape) { + nav a, + nav button { + padding: 2rem 0; + } +} .view { background-color: var(--bgcolor); @@ -61,20 +74,25 @@ nav { transition: transform .3s cubic-bezier(.465,.183,.153,.946); width: 100%; will-change: transform; + z-index: 2; } @media (orientation: landscape) { .view { transition: opacity .3s cubic-bezier(.465,.183,.153,.946); } } +.view.view-next { + z-index: 3; +} +.view.view-prev { + z-index: 1; +} @media (orientation: portrait) { .view.view-next { transform: translateX(100%); } .view.view-prev { - position: relative; transform: translateX(-20%); - z-index: 0; } } @media (orientation: landscape) { @@ -91,7 +109,7 @@ nav { flex-grow: 1; margin-inline: auto; overflow-y: auto; - padding: var(--gap-half) 0; + padding: var(--gap-half) 0 0 0; width: 100%; } main .content { @@ -108,3 +126,7 @@ nav a { text-align: center; text-decoration: none; } + +.content > header { + padding: 3rem 3rem 3rem calc(var(--profileimg-size) + var(--gap)); +} diff --git a/src/ui.ts b/src/ui.ts new file mode 100644 index 0000000..862b5a6 --- /dev/null +++ b/src/ui.ts @@ -0,0 +1,84 @@ +import {Event} from 'nostr-tools'; +import {elem, elemArticle, parseTextContent} from './utils/dom'; +import {dateTime, formatTime} from './utils/time'; +import {validatePow, sortByCreatedAt} from './events'; +import {setViewElem} from './view'; +import {config} from './settings'; +import {getReactions, getReactionContents} from './reactions'; +import {openWriteInput} from './write'; +import {linkPreview} from './media'; +import {getMetadata} from './profiles'; +import {EventWithNip19, replyList} from './notes'; + +setInterval(() => { + document.querySelectorAll('time[datetime]').forEach((timeElem: HTMLTimeElement) => { + timeElem.textContent = formatTime(new Date(timeElem.dateTime)); + }); +}, 10000); + +export const createTextNote = ( + evt: EventWithNip19, + relay: string, +) => { + const {host, img, name, time, userName} = getMetadata(evt, relay); + const replies = replyList.filter(({replyTo}) => replyTo === evt.id); + // const isLongContent = evt.content.trimRight().length > 280; + // const content = isLongContent ? evt.content.slice(0, 280) : evt.content; + const reactions = getReactions(evt.id); + const didReact = reactions.length && !!reactions.find(reaction => reaction.pubkey === config.pubkey); + const [content, {firstLink}] = parseTextContent(evt.content); + const buttons = elem('div', {className: 'buttons'}, [ + elem('button', {name: 'reply', type: 'button'}, [ + elem('img', {height: 24, width: 24, src: '/assets/comment.svg'}) + ]), + elem('button', {name: 'star', type: 'button'}, [ + elem('img', { + alt: didReact ? '✭' : '✩', // ♥ + height: 24, width: 24, + src: `/assets/${didReact ? 'star-fill' : 'star'}.svg`, + title: getReactionContents(evt.id).join(' '), + }), + elem('small', {data: {reactions: ''}}, reactions.length || ''), + ]), + ]); + if (localStorage.getItem('reply_to') === evt.id) { + openWriteInput(buttons, evt.id); + } + const replyFeed: Array = replies[0] ? replies.sort(sortByCreatedAt).map(e => setViewElem(e.id, createTextNote(e, relay))) : []; + return elemArticle([ + elem('div', {className: 'mbox-img'}, img), + elem('div', {className: 'mbox-body'}, [ + elem('header', { + className: 'mbox-header', + title: `User: ${userName}\n${time}\n\nUser pubkey: ${evt.pubkey}\n\nRelay: ${host}\n\nEvent-id: ${evt.id} + ${evt.tags.length ? `\nTags ${JSON.stringify(evt.tags)}\n` : ''} + ${evt.content}` + }, [ + elem('a', {className: `mbox-username${name ? ' mbox-kind0-name' : ''}`, href: `/${evt.nip19.npub}`}, name || userName), + ' ', + elem('a', {href: `/${evt.nip19.note}`}, elem('time', {dateTime: time.toISOString()}, formatTime(time))), + ]), + elem('div', {/* data: isLongContent ? {append: evt.content.slice(280)} : null*/}, [ + ...content, + (firstLink && validatePow(evt)) ? linkPreview(firstLink, evt.id, relay) : null, + ]), + buttons, + ]), + ...(replies[0] ? [elem('div', {className: 'mobx-replies'}, replyFeed.reverse())] : []), + ], {data: {id: evt.id, pubkey: evt.pubkey, relay}}); +}; + +export const renderRecommendServer = (evt: Event, relay: string) => { + const {img, name, time, userName} = getMetadata(evt, relay); + const body = elem('div', {className: 'mbox-body', title: dateTime.format(time)}, [ + elem('header', {className: 'mbox-header'}, [ + elem('small', {}, [ + elem('strong', {}, userName) + ]), + ]), + ` recommends server: ${evt.content}`, + ]); + return elemArticle([ + elem('div', {className: 'mbox-img'}, [img]), body + ], {className: 'mbox-recommend-server', data: {id: evt.id, pubkey: evt.pubkey}}); +}; diff --git a/src/view.ts b/src/view.ts index 79d8b53..445b07f 100644 --- a/src/view.ts +++ b/src/view.ts @@ -1,12 +1,25 @@ import {elem} from './utils/dom'; +type ViewOptions = { + type: 'feed' +} | { + type: 'note'; + id: string; +} | { + type: 'profile'; + id: string; +}; + +type DOMMap = { + [id: string]: HTMLElement +}; + type Container = { id: string; + options: ViewOptions, view: HTMLElement; content: HTMLDivElement; - dom: { - [eventId: string]: HTMLElement - } + dom: DOMMap; }; const containers: Array = []; @@ -22,37 +35,56 @@ export const clearView = () => { getViewContent().replaceChildren(); }; -export const getViewElem = (eventId: string) => { - return containers[activeContainerIndex]?.dom[eventId]; +export const getViewElem = (id: string) => { + return containers[activeContainerIndex]?.dom[id]; }; -export const setViewElem = (eventId: string, node: HTMLElement) => { +export const setViewElem = (id: string, node: HTMLElement) => { const container = containers[activeContainerIndex]; if (container) { - container.dom[eventId] = node; + container.dom[id] = node; } return node; }; -const mainContainer = document.querySelector('main'); +const mainContainer = document.querySelector('main') as HTMLElement; -const getContainer = (route: string) => { - let container = containers.find(c => c.id === route); - if (container) { - return container; - } +const createContainer = ( + route: string, + options: ViewOptions, +) => { const content = elem('div', {className: 'content'}); + const dom: DOMMap = {}; + switch (options.type) { + case 'profile': + const header = elem('header', {}, + elem('small', {}, route) + ); + dom[options.id] = header; + content.append(header); + break; + case 'note': + break; + case 'feed': + break; + } const view = elem('section', {className: 'view'}, [content]); - mainContainer?.append(view); - container = {id: route, view, content, dom: {}}; + const container = {id: route, options, view, content, dom}; + mainContainer.append(view); containers.push(container); return container; }; -export const view = (route: string) => { +type GetViewOptions = () => ViewOptions; + +export const getViewOptions: GetViewOptions = () => containers[activeContainerIndex]?.options || {type: 'feed'}; + +export const view = ( + route: string, + options: ViewOptions, +) => { const active = containers[activeContainerIndex]; - active?.view.classList.remove('view-active'); - const nextContainer = getContainer(route); + const nextContainer = containers.find(c => c.id === route) || createContainer(route, options); const nextContainerIndex = containers.indexOf(nextContainer); if (nextContainerIndex === activeContainerIndex) { return; @@ -63,12 +95,6 @@ export const view = (route: string) => { requestAnimationFrame(() => { requestAnimationFrame(() => { nextContainer.view.classList.remove('view-next', 'view-prev'); - nextContainer.view.classList.add('view-active'); - }); - // // console.log(activeContainerIndex, nextContainerIndex); - getViewContent()?.querySelectorAll('.view-prev').forEach(prev => { - prev.classList.remove('view-prev'); - prev.classList.add('view-next'); }); active?.view.classList.add(nextContainerIndex < activeContainerIndex ? 'view-next' : 'view-prev'); activeContainerIndex = nextContainerIndex;