Skip to content

Commit

Permalink
Refactor feed & new views
Browse files Browse the repository at this point in the history
  • Loading branch information
ChiriVulpes committed Jan 29, 2025
1 parent 5bed5fa commit 3714d64
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 147 deletions.
5 changes: 3 additions & 2 deletions lang/en-nz.quilt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ comment-chapter: {TRIGGERED_BY} has commented on {CHAPTER} of {WORK}
comment-reply: {TRIGGERED_BY} has replied to your comment on {CHAPTER} of {WORK}
comment-mention: {TRIGGERED_BY} has mentioned you in a comment on {CHAPTER} of {WORK}

# work-feed
empty: And all was quiet...



# view
Expand All @@ -215,11 +218,9 @@ description-404: Your adventure has come to a close, your questions left unanswe

## new
main/title: Recent Updates
content/empty: And all was quiet...

## feed
main/title: Following
content/empty: And all was quiet...

## account
name/label=shared/form/name/label
Expand Down
1 change: 1 addition & 0 deletions src/endpoint/Endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ interface Endpoint<ROUTE extends keyof Paths, QUERY extends EndpointQuery<ROUTE>
noResponse (): this
query: QUERY
prep: (...parameters: Parameters<QUERY>) => ConfigurablePreparedEndpointQuery<ROUTE, QUERY>
getPageSize?(): number | undefined
}

interface ConfigurablePreparedEndpointQuery<ROUTE extends keyof Paths, QUERY extends EndpointQuery<ROUTE>> extends PreparedEndpointQuery<ROUTE, QUERY> {
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Object.assign(window, {
// @ts-expect-error no types
import sourceMapSupport from 'browser-source-map-support'
import Env from 'utility/Env'
import Maps from 'utility/Maps'
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
sourceMapSupport.install({
environment: 'browser',
Expand All @@ -35,6 +36,7 @@ document.startViewTransition ??= cb => {

applyDOMRectPrototypes()
Arrays.applyPrototypes()
Maps.applyPrototypes()
Elements.applyPrototypes()

void (async () => {
Expand Down
212 changes: 118 additions & 94 deletions src/model/PagedListData.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { PaginatedEndpoint, PreparedPaginatedQueryReturning, PreparedQueryOf } from 'endpoint/Endpoint'
import type { PagedDataDefinition } from 'model/PagedData'
import PagedData from 'model/PagedData'
import State from 'utility/State'
Expand All @@ -8,117 +9,140 @@ interface PagedListData<T> extends PagedData<T[]> {
resized (pageSize: number): PagedListData<T>
}

function PagedListData<T> (pageSize: number, definition: PagedDataDefinition<T[]>): PagedListData<T> {
const list = PagedData<T[]>(definition)
return Object.assign(
list,
{
pageSize,
resized (resizePageSize: number) {
const newList = PagedListData(resizePageSize, {
get: getPage,
})

list.rawPages.subscribeManual(() => {
// go through current pages and update each from the source list
const pages = newList.rawPages.value
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
if (State.is(page)) {
const pageState = page
const PagedListData = Object.assign(
function <T> (pageSize: number, definition: PagedDataDefinition<T[]>): PagedListData<T> {
const list = PagedData<T[]>(definition)
return Object.assign(
list,
{
pageSize,
////////////////////////////////////
//#region View Resizing
resized (resizePageSize: number) {
const newList = PagedListData(resizePageSize, {
get: getPage,
})

list.rawPages.subscribeManual(() => {
// go through current pages and update each from the source list
const pages = newList.rawPages.value
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
if (State.is(page)) {
const pageState = page
const value = getPage(i)
if (value instanceof Promise)
void value.then(setPageValue)
else
setPageValue(value)

function setPageValue (value: false | State<T[] | null>) {
if (State.is(value))
pageState.bindManual(value)
else
pageState.value = value

newList.rawPages.emit()
}

continue
}

const value = getPage(i)
if (value instanceof Promise)
void value.then(setPageValue)
pages[i] = value.then(setPage)
else
setPageValue(value)
pages[i] = setPage(value)
newList.rawPages.emit()

function setPageValue (value: false | State<T[] | null>) {
function setPage (value: false | State<T[] | null>) {
const state = pages[i] = State<T[] | null | false>(null, false)// .setId('PagedListData subscribeManual setPage')
if (State.is(value))
pageState.bindManual(value)
state.bindManual(value)
else
pageState.value = value

state.value = value
newList.rawPages.emit()
return state
}

continue
}

const value = getPage(i)
if (value instanceof Promise)
pages[i] = value.then(setPage)
else
pages[i] = setPage(value)
newList.rawPages.emit()

function setPage (value: false | State<T[] | null>) {
const state = pages[i] = State<T[] | null | false>(null, false)// .setId('PagedListData subscribeManual setPage')
if (State.is(value))
state.bindManual(value)
else
state.value = value
newList.rawPages.emit()
return state
}
}
})
})

const mutableNewList = newList as PartialMutable<PagedListData<T>>
delete mutableNewList.resized
const mutableNewList = newList as PartialMutable<PagedListData<T>>
delete mutableNewList.resized

return newList
return newList

type SourcePage = State.Mutable<false | T[] | null>
type SourcePages = SourcePage[]
type SourcePage = State.Mutable<false | T[] | null>
type SourcePages = SourcePage[]

function getPage (page: number): PromiseOr<State<T[] | null> | false> {
const start = page * resizePageSize
const end = (page + 1) * resizePageSize
const startPageInSource = Math.floor(start / pageSize)
const endPageInSource = Math.ceil(end / pageSize)
const startIndexInFirstSourcePage = start % pageSize
const endIndexInLastSourcePage = (end % pageSize) || pageSize
function getPage (page: number): PromiseOr<State<T[] | null> | false> {
const start = page * resizePageSize
const end = (page + 1) * resizePageSize
const startPageInSource = Math.floor(start / pageSize)
const endPageInSource = Math.ceil(end / pageSize)
const startIndexInFirstSourcePage = start % pageSize
const endIndexInLastSourcePage = (end % pageSize) || pageSize

const rawPages: PromiseOr<SourcePage | undefined>[] = list.rawPages.value.slice()
for (let i = startPageInSource; i < endPageInSource; i++) {
const rawPage = rawPages[i]
if (i >= startPageInSource && i < endPageInSource && (!rawPage || (State.is(rawPage) && rawPage.value === false))) {
rawPages[i] = list.get(i) as PromiseOr<State.Mutable<T[] | false | null>>
const rawPages: PromiseOr<SourcePage | undefined>[] = list.rawPages.value.slice()
for (let i = startPageInSource; i < endPageInSource; i++) {
const rawPage = rawPages[i]
if (i >= startPageInSource && i < endPageInSource && (!rawPage || (State.is(rawPage) && rawPage.value === false))) {
rawPages[i] = list.get(i) as PromiseOr<State.Mutable<T[] | false | null>>
}
}
}

const sourcePages = rawPages.slice(startPageInSource, endPageInSource)
if (sourcePages.some(page => page instanceof Promise))
return Promise.all(sourcePages).then(sourcePages => resolveData(sourcePages, startIndexInFirstSourcePage, endIndexInLastSourcePage))

return resolveData(sourcePages as SourcePages, startIndexInFirstSourcePage, endIndexInLastSourcePage)
}

function resolveData (sourcePages: (SourcePage | undefined)[], startIndex: number, endIndex: number): State<T[] | null> | false {
const data: T[] = []
for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i]
if (sourcePage?.value === false)
return false

if (!sourcePage?.value)
continue

if (i === 0 && i === sourcePages.length - 1)
data.push(...sourcePage.value.slice(startIndex, endIndex))
else if (i === 0)
data.push(...sourcePage.value.slice(startIndex))
else if (i === sourcePages.length - 1)
data.push(...sourcePage.value.slice(0, endIndex))
else
data.push(...sourcePage.value)
const sourcePages = rawPages.slice(startPageInSource, endPageInSource)
if (sourcePages.some(page => page instanceof Promise))
return Promise.all(sourcePages).then(sourcePages => resolveData(sourcePages, startIndexInFirstSourcePage, endIndexInLastSourcePage))

return resolveData(sourcePages as SourcePages, startIndexInFirstSourcePage, endIndexInLastSourcePage)
}

return State.Generator(() => data, false).observeManual(...sourcePages)// .setId('PagedListData resolveData')
}
},
}
)
}
function resolveData (sourcePages: (SourcePage | undefined)[], startIndex: number, endIndex: number): State<T[] | null> | false {
const data: T[] = []
for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i]
if (sourcePage?.value === false)
return false

if (!sourcePage?.value)
continue

if (i === 0 && i === sourcePages.length - 1)
data.push(...sourcePage.value.slice(startIndex, endIndex))
else if (i === 0)
data.push(...sourcePage.value.slice(startIndex))
else if (i === sourcePages.length - 1)
data.push(...sourcePage.value.slice(0, endIndex))
else
data.push(...sourcePage.value)
}

return State.Generator(() => data, false).observeManual(...sourcePages)// .setId('PagedListData resolveData')
}
},
//#endregion
////////////////////////////////////
}
)
},
{
fromEndpoint<T> (pageSize: number, endpoint: PreparedPaginatedQueryReturning<T>): PagedListData<T> {
const e = endpoint as PreparedQueryOf<PaginatedEndpoint>
return PagedListData(pageSize, {
async get (page) {
const response = await e.query(undefined, { page })
if (toast.handleError(response))
return false

if (!Array.isArray(response.data) || response.data.length)
return response.data as T[]

return null
},
})
},
}
)

export default PagedListData
18 changes: 18 additions & 0 deletions src/ui/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ interface BaseComponent<ELEMENT extends HTMLElement = HTMLElement> extends Compo
extendMagic<K extends Exclude<keyof this, symbol>, O extends this = this> (property: K, magic: (component: this) => { get (): O[K], set?(value: O[K]): void }): this
extendJIT<K extends Exclude<keyof this, symbol>, O extends this = this> (property: K, supplier: (component: this) => O[K]): this
override<K extends keyof this> (property: K, provider: (component: this, original: this[K]) => this[K]): this
tweakJIT<PARAMS extends any[], K extends Exclude<keyof this, symbol>, O extends this = this> (property: K, tweaker: (value: O[K], component: this) => unknown): this

tweak<PARAMS extends any[]> (tweaker?: (component: this, ...params: PARAMS) => unknown, ...params: PARAMS): this

Expand Down Expand Up @@ -236,6 +237,8 @@ function Component (type: keyof HTMLElementTagNameMap = 'span'): Component {

let descendantsListeningForScroll: HTMLCollection | undefined

const jitTweaks = new Map<string, true | Set<(value: any, component: Component) => unknown>>()

let component = ({
supers: State([]),
isComponent: true,
Expand Down Expand Up @@ -317,6 +320,11 @@ function Component (type: keyof HTMLElementTagNameMap = 'span'): Component {
get: () => {
const value = supplier(component)
Define.set(component, property, value)
const tweaks = jitTweaks.get(property)
if (tweaks && tweaks !== true)
for (const tweaker of tweaks)
tweaker(value, component)
jitTweaks.set(property, true)
return value
},
set: value => {
Expand All @@ -326,6 +334,16 @@ function Component (type: keyof HTMLElementTagNameMap = 'span'): Component {
return component
},

tweakJIT: (property, tweaker) => {
const tweaks = jitTweaks.compute(property, () => new Set())
if (tweaks === true)
tweaker(component[property] as never, component)
else
tweaks.add(tweaker)

return component
},

tweak: (tweaker: (component: Component, ...params: any[]) => unknown, ...params: any[]) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
tweaker?.(component, ...params)
Expand Down
67 changes: 67 additions & 0 deletions src/ui/component/WorkFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { Author, Feed, Work as WorkData } from 'api.fluff4.me'
import type { PreparedPaginatedQueryReturning } from 'endpoint/Endpoint'
import PagedListData from 'model/PagedListData'
import Component from 'ui/Component'
import Link from 'ui/component/core/Link'
import Paginator2 from 'ui/component/core/Paginator2'
import Work from 'ui/component/Work'
import State from 'utility/State'

interface WorkFeedExtensions {
setFromEndpoint (endpoint: PreparedPaginatedQueryReturning<Feed>): this
setFromWorks (pagedData: PagedListData<WorkData>, authors: Author[]): this
}

interface WorkFeed extends Paginator2, WorkFeedExtensions { }

const WorkFeed = Component.Builder((component): WorkFeed => {
const paginator = component.and(Paginator2)
.type('flush')

const set = paginator.set

const feed = paginator.extend<WorkFeedExtensions>(feed => ({
setFromEndpoint (endpoint) {
const authors = State<Author[]>([])
const data = PagedListData(endpoint.getPageSize?.() ?? 25, {
async get (page) {
const response = await endpoint.query(undefined, { page })
if (toast.handleError(response))
return false

if (!Array.isArray(response.data) || response.data.length) {
authors.value.push(...response.data.authors)
authors.value.distinctInPlace(author => author.vanity)
authors.emit()
return response.data.works
}

return null
},
})
feed.setFromWorks(data.resized(3), authors.value)
return feed
},
setFromWorks (pagedData, authors) {
set(pagedData, (slot, works) => {
for (const workData of works) {
const author = authors.find(author => author.vanity === workData.author)
Link(author && `/work/${author.vanity}/${workData.vanity}`)
.and(Work, workData, author, true)
.viewTransition()
.appendTo(slot)
}
})
return feed
},
}))

paginator.orElse(slot => Component()
.style('placeholder')
.text.use('work-feed/empty')
.appendTo(slot))

return feed
})

export default WorkFeed
Loading

0 comments on commit 3714d64

Please sign in to comment.