Skip to content

Commit a9fc232

Browse files
committed
fix post fetch
1 parent a022eed commit a9fc232

File tree

7 files changed

+292
-35
lines changed

7 files changed

+292
-35
lines changed

src/client/db/remote-adapter.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ky from 'ky'
77
import type {
88
DbAdapter,
99
Feed,
10+
Item,
1011
ItemsResponse,
1112
CountsResponse
1213
} from './types.js'
@@ -64,6 +65,29 @@ export const remoteAdapter:DbAdapter = {
6465
return response.json<ItemsResponse>()
6566
},
6667

68+
async getItemByRoute (itemRoute:string):Promise<Item|null> {
69+
try {
70+
const response = await api.get('items/by-route', {
71+
searchParams: { route: itemRoute }
72+
})
73+
74+
const data = await response.json<{
75+
item:Item
76+
}>()
77+
return data.item
78+
} catch (err) {
79+
if (
80+
err instanceof Error &&
81+
'response' in err &&
82+
(err as { response?:Response }).response?.status === 404
83+
) {
84+
return null
85+
}
86+
87+
throw err
88+
}
89+
},
90+
6791
async getCounts ():Promise<CountsResponse> {
6892
const response = await api.get('items/count')
6993
return response.json<CountsResponse>()

src/client/db/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export interface DbAdapter {
5858
limit?:number
5959
offset?:number
6060
}):Promise<ItemsResponse>
61+
getItemByRoute(itemRoute:string):Promise<Item|null>
6162
getCounts():Promise<CountsResponse>
6263
updateItem(
6364
id:number,

src/client/routes/index.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,7 @@ export default function _Router (state:AppState):InstanceType<typeof Router> {
5858
return FeedReader
5959
})
6060

61-
router.addRoute('/post/*', (match:ReturnType<typeof router.match>) => {
62-
const splats = match!.splats
63-
const itemUrl = splats[0]
64-
const item = state.items.value.find(i => i.link?.includes(itemUrl))
65-
if (item && !item.is_read) {
66-
State.toggleItemRead(state, item.id, true)
67-
}
61+
router.addRoute('/post/*', () => {
6862
return ItemReader
6963
})
7064

src/client/routes/item-reader.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,64 @@ import { useCallback } from 'preact/hooks'
44
import { useComputed } from '@preact/signals'
55
import { NotFound } from '../not-found.js'
66
import { formatDate, sanitizeHtml } from '../util.js'
7-
import { type Item, type AppState, State } from '../state.js'
7+
import {
8+
type Item,
9+
type AppState,
10+
State,
11+
findItemByRoute,
12+
isItemRoute
13+
} from '../state.js'
814
import './item-reader.css'
915
import Debug from '@substrate-system/debug'
1016
const debug = Debug('rsss:view')
1117

1218
export const ItemReader:FunctionComponent<{
1319
state:AppState;
1420
splats:string[];
15-
}> = function ItemReader ({ state, splats }) {
16-
const itemUrl = splats.shift()
17-
18-
debug('reading...', itemUrl)
21+
}> = function ItemReader ({ state }) {
22+
debug('reading...', state.route.value)
1923

2024
const itemSignal = useComputed<
2125
undefined|null|Item
2226
>(() => {
23-
if (!state.items.value.length) return null
24-
return state.items.value.find(
25-
i => i.link?.includes(itemUrl!)
27+
if (!isItemRoute(state.route.value)) return null
28+
return (
29+
findItemByRoute(state, state.route.value) ||
30+
state.routeItem.value
2631
)
2732
})
2833

2934
const item = itemSignal.value
30-
if (!item) {
31-
return html`<${NotFound} />`
35+
const isLoadingItem = (
36+
state.routeItemLoading.value &&
37+
!item
38+
)
39+
40+
if (isLoadingItem) {
41+
return html`
42+
<div class="route item-reader">
43+
<p class="loading-text">Loading post...</p>
44+
</div>
45+
`
3246
}
47+
if (!item) return html`<${NotFound} />`
3348

49+
const itemId = item.id
3450
const isStarred = !!item.is_starred
3551
const isRead = !!item.is_read
3652

3753
const handleStar = useCallback(async () => {
3854
await State.toggleItemStarred(
3955
state,
40-
item!.id,
56+
itemId,
4157
!isStarred
4258
)
43-
}, [])
59+
}, [itemId, isStarred])
4460

4561
async function handleToggleRead () {
4662
await State.toggleItemRead(
4763
state,
48-
item!.id,
64+
itemId,
4965
!isRead
5066
)
5167
}

src/client/state.ts

Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export interface CountsResponse {
6161
export type AppState = {
6262
_setRoute:(route:string) => void,
6363
route:Signal<string>,
64+
routeItem:Signal<Item|null>,
65+
routeItemLoading:Signal<boolean>,
6466
user:Signal<User|null>,
6567
authLoading:Signal<boolean>,
6668
authError:Signal<string|null>,
@@ -84,6 +86,8 @@ export function State ():AppState {
8486
const state = {
8587
_setRoute: onRoute.setRoute.bind(onRoute),
8688
route: signal(location.pathname),
89+
routeItem: signal<Item|null>(null),
90+
routeItemLoading: signal(false),
8791
user: signal<User|null>(null),
8892
authLoading: signal(true),
8993
authError: signal<string|null>(null),
@@ -114,6 +118,82 @@ export function State ():AppState {
114118
}
115119
})
116120

121+
let routeItemRequest:string|null = null
122+
123+
effect(() => {
124+
const route = state.route.value
125+
const itemFromList = findItemByRoute(state, route)
126+
127+
if (!isItemRoute(route)) {
128+
routeItemRequest = null
129+
batch(() => {
130+
state.routeItem.value = null
131+
state.routeItemLoading.value = false
132+
})
133+
return
134+
}
135+
136+
if (itemFromList) {
137+
routeItemRequest = null
138+
batch(() => {
139+
state.routeItem.value = itemFromList
140+
state.routeItemLoading.value = false
141+
})
142+
143+
if (!itemFromList.is_read) {
144+
void State.toggleItemRead(
145+
state,
146+
itemFromList.id,
147+
true
148+
)
149+
}
150+
return
151+
}
152+
153+
if (!state.isAuthenticated.value) {
154+
batch(() => {
155+
state.routeItem.value = null
156+
state.routeItemLoading.value = false
157+
})
158+
return
159+
}
160+
161+
if (routeItemRequest === route) return
162+
routeItemRequest = route
163+
state.routeItemLoading.value = true
164+
165+
void State.loadItemByRoute(state, route)
166+
.then((item) => {
167+
if (state.route.value !== route) return
168+
169+
batch(() => {
170+
state.routeItem.value = item
171+
state.routeItemLoading.value = false
172+
})
173+
174+
if (item && !item.is_read) {
175+
void State.toggleItemRead(
176+
state,
177+
item.id,
178+
true
179+
)
180+
}
181+
})
182+
.catch((err) => {
183+
debug('Error loading route item:', err)
184+
if (state.route.value !== route) return
185+
batch(() => {
186+
state.routeItem.value = null
187+
state.routeItemLoading.value = false
188+
})
189+
})
190+
.finally(() => {
191+
if (routeItemRequest === route) {
192+
routeItemRequest = null
193+
}
194+
})
195+
})
196+
117197
/**
118198
* Load data after authentication
119199
*/
@@ -492,6 +572,27 @@ State.loadItems = async function (
492572
}
493573
}
494574

575+
/**
576+
* Load a single item that matches a /post/* route
577+
*/
578+
State.loadItemByRoute = async function (
579+
_state:AppState,
580+
route:string
581+
):Promise<Item|null> {
582+
const itemRoute = routeToItemRoute(route)
583+
if (!itemRoute) return null
584+
585+
try {
586+
const item = await remoteAdapter.getItemByRoute(
587+
itemRoute
588+
)
589+
return item as Item|null
590+
} catch (err) {
591+
debug('Error loading item by route:', err)
592+
return null
593+
}
594+
}
595+
495596
/**
496597
* Load counts from remote DB
497598
*/
@@ -521,12 +622,21 @@ State.toggleItemRead = async function (
521622

522623
if (response.ok) {
523624
// Optimistic UI update
524-
state.items.value = state.items.value.map(
525-
item => item.id === itemId ? {
526-
...item,
527-
is_read: isRead ? 1 : 0
528-
} : item
529-
)
625+
batch(() => {
626+
state.items.value = state.items.value.map(
627+
item => item.id === itemId ? {
628+
...item,
629+
is_read: isRead ? 1 : 0
630+
} : item
631+
)
632+
633+
if (state.routeItem.value?.id === itemId) {
634+
state.routeItem.value = {
635+
...state.routeItem.value,
636+
is_read: isRead ? 1 : 0
637+
}
638+
}
639+
})
530640

531641
await State.loadCounts(state)
532642
}
@@ -556,6 +666,13 @@ State.toggleItemStarred = async function (
556666
is_starred: isStarred ? 1 : 0
557667
} : item
558668
)
669+
670+
if (state.routeItem.value?.id === itemId) {
671+
state.routeItem.value = {
672+
...state.routeItem.value,
673+
is_starred: isStarred ? 1 : 0
674+
}
675+
}
559676
})
560677

561678
await State.loadCounts(state)
@@ -626,15 +743,17 @@ State.clearSelectedItem = function (state:AppState):void {
626743
export const isItemRoute = function (
627744
route:string
628745
):boolean {
629-
if (
630-
route === '/' ||
631-
route.startsWith('/login') ||
632-
route.startsWith('/api')
633-
) {
634-
return false
635-
}
746+
return route.startsWith('/post/')
747+
}
636748

637-
return route.includes('/post/')
749+
/**
750+
* Convert a /post/* route to the comparable link fragment
751+
*/
752+
export const routeToItemRoute = function (
753+
route:string
754+
):string|null {
755+
if (!isItemRoute(route)) return null
756+
return route.replace(/^\/post\//, '')
638757
}
639758

640759
/**
@@ -644,8 +763,16 @@ export const findItemByRoute = function (
644763
state:AppState,
645764
route:string
646765
):Item|null {
766+
const itemRoute = routeToItemRoute(route)
767+
if (!itemRoute) return null
768+
647769
for (const item of state.items.value) {
648-
if (itemToRoute(item) === route) {
770+
const itemRoutePath = itemToRoute(item)
771+
if (itemRoutePath === route) {
772+
return item
773+
}
774+
775+
if (item.link?.includes(itemRoute)) {
649776
return item
650777
}
651778
}

0 commit comments

Comments
 (0)