modified internal/server/articles_handler.go +8 -3
| @@ -152,12 +152,17 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) { |
| 152 | 152 | nextID *int64 |
| 153 | 153 | ) |
| 154 | 154 | |
| 155 | + var isRead bool |
| 156 | + |
| 155 | 157 | g, gCtx := errgroup.WithContext(ctx) |
| 156 | 158 | |
| 157 | 159 | g.Go(func() error { |
| 158 | | - if err := s.dbs.Articles.MarkArticleRead(gCtx, user.DID, id); err != nil { |
| 159 | | - s.logger.Warn("failed to mark article read", "error", err, "id", id) |
| 160 | + state, err := s.dbs.Articles.GetReadState(gCtx, user.DID, id) |
| 161 | + if err != nil { |
| 162 | + s.logger.Warn("failed to get read state", "error", err, "id", id) |
| 163 | + return nil |
| 160 | 164 | } |
| 165 | + isRead = state.IsRead |
| 161 | 166 | return nil |
| 162 | 167 | }) |
| 163 | 168 | |
| @@ -215,7 +220,7 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) { |
| 215 | 220 | _ = g.Wait() |
| 216 | 221 | |
| 217 | 222 | dto := toArticle(article) |
| 218 | | - dto.IsRead = true |
| 223 | + dto.IsRead = isRead |
| 219 | 224 | dto.LikeCount = likeCount |
| 220 | 225 | dto.HasLiked = liked |
| 221 | 226 | |
| @@ -152,12 +152,17 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) { |
| 152 | nextID *int64 | 152 | nextID *int64 |
| 153 | ) | 153 | ) |
| 154 | | 154 | |
| | 155 | + var isRead bool |
| | 156 | + |
| 155 | g, gCtx := errgroup.WithContext(ctx) | 157 | g, gCtx := errgroup.WithContext(ctx) |
| 156 | | 158 | |
| 157 | g.Go(func() error { | 159 | g.Go(func() error { |
| 158 | - if err := s.dbs.Articles.MarkArticleRead(gCtx, user.DID, id); err != nil { | 160 | + state, err := s.dbs.Articles.GetReadState(gCtx, user.DID, id) |
| 159 | - s.logger.Warn("failed to mark article read", "error", err, "id", id) | 161 | + if err != nil { |
| | 162 | + s.logger.Warn("failed to get read state", "error", err, "id", id) |
| | 163 | + return nil |
| 160 | } | 164 | } |
| | 165 | + isRead = state.IsRead |
| 161 | return nil | 166 | return nil |
| 162 | }) | 167 | }) |
| 163 | | 168 | |
| @@ -215,7 +220,7 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) { |
| 215 | _ = g.Wait() | 220 | _ = g.Wait() |
| 216 | | 221 | |
| 217 | dto := toArticle(article) | 222 | dto := toArticle(article) |
| 218 | - dto.IsRead = true | 223 | + dto.IsRead = isRead |
| 219 | dto.LikeCount = likeCount | 224 | dto.LikeCount = likeCount |
| 220 | dto.HasLiked = liked | 225 | dto.HasLiked = liked |
| 221 | | 226 | |
modified web/src/routes/articles/[id]/+page.svelte +18 -0
| @@ -58,6 +58,24 @@ |
| 58 | 58 | annotations = data.annotations; |
| 59 | 59 | }); |
| 60 | 60 | |
| 61 | + // Sync local optimistic state when navigating between articles. |
| 62 | + $effect(() => { |
| 63 | + read = data.article.is_read; |
| 64 | + }); |
| 65 | + |
| 66 | + // Mark the article read on actual navigation. The server's detail handler |
| 67 | + // no longer marks read, so a hover/touch preload won't mark every hovered |
| 68 | + // card as read; only a real visit does. |
| 69 | + $effect(() => { |
| 70 | + const id = data.article.id; |
| 71 | + if (!read) { |
| 72 | + read = true; |
| 73 | + endpoints.markRead(id).catch(() => { |
| 74 | + if (data.article.id === id) read = false; |
| 75 | + }); |
| 76 | + } |
| 77 | + }); |
| 78 | + |
| 61 | 79 | function clamp(v: number, min: number, max: number) { |
| 62 | 80 | return Math.max(min, Math.min(max, v)); |
| 63 | 81 | } |
| @@ -58,6 +58,24 @@ |
| 58 | annotations = data.annotations; | 58 | annotations = data.annotations; |
| 59 | }); | 59 | }); |
| 60 | | 60 | |
| | 61 | + // Sync local optimistic state when navigating between articles. |
| | 62 | + $effect(() => { |
| | 63 | + read = data.article.is_read; |
| | 64 | + }); |
| | 65 | + |
| | 66 | + // Mark the article read on actual navigation. The server's detail handler |
| | 67 | + // no longer marks read, so a hover/touch preload won't mark every hovered |
| | 68 | + // card as read; only a real visit does. |
| | 69 | + $effect(() => { |
| | 70 | + const id = data.article.id; |
| | 71 | + if (!read) { |
| | 72 | + read = true; |
| | 73 | + endpoints.markRead(id).catch(() => { |
| | 74 | + if (data.article.id === id) read = false; |
| | 75 | + }); |
| | 76 | + } |
| | 77 | + }); |
| | 78 | + |
| 61 | function clamp(v: number, min: number, max: number) { | 79 | function clamp(v: number, min: number, max: number) { |
| 62 | return Math.max(min, Math.min(max, v)); | 80 | return Math.max(min, Math.min(max, v)); |
| 63 | } | 81 | } |