@@ -61,6 +61,8 @@ export interface CountsResponse {
6161export 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 {
626743export 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 ( / ^ \/ p o s t \/ / , '' )
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