aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-08-22 23:17:09 +0900
committernsfisis <nsfisis@gmail.com>2025-08-22 23:17:09 +0900
commit987f78ac25af119796c6f3830a61aafcabeb2dc1 (patch)
treea96e510e28c389a14f700aceb9ab71b3edbb8b71
parent0f0a08196a045d49cf98bb2b841e6475cc0c94a6 (diff)
downloadfeedaka-987f78ac25af119796c6f3830a61aafcabeb2dc1.tar.gz
feedaka-987f78ac25af119796c6f3830a61aafcabeb2dc1.tar.zst
feedaka-987f78ac25af119796c6f3830a61aafcabeb2dc1.zip
fix(frontend): wrong optimistic update
-rw-r--r--frontend/src/components/ArticleItem.tsx38
-rw-r--r--frontend/src/components/ArticleList.tsx28
-rw-r--r--frontend/src/pages/ReadArticles.tsx4
-rw-r--r--frontend/src/pages/UnreadArticles.tsx4
4 files changed, 43 insertions, 31 deletions
diff --git a/frontend/src/components/ArticleItem.tsx b/frontend/src/components/ArticleItem.tsx
index faa86fe..4942518 100644
--- a/frontend/src/components/ArticleItem.tsx
+++ b/frontend/src/components/ArticleItem.tsx
@@ -1,6 +1,5 @@
import { faCheck, faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useOptimistic } from "react";
import { useMutation } from "urql";
import type {
GetReadArticlesQuery,
@@ -18,27 +17,19 @@ type Article = NonNullable<
interface Props {
article: Article;
+ onReadChange?: (articleId: string, isRead: boolean) => void;
}
-export function ArticleItem({ article }: Props) {
+export function ArticleItem({ article, onReadChange }: Props) {
const [, markArticleRead] = useMutation(MarkArticleReadDocument);
const [, markArticleUnread] = useMutation(MarkArticleUnreadDocument);
- const [optimisticArticle, setOptimisticArticle] = useOptimistic(
- article,
- (currentArticle, newReadState: boolean) => ({
- ...currentArticle,
- isRead: newReadState,
- }),
- );
-
const handleToggleRead = async (
articleId: string,
isCurrentlyRead: boolean,
) => {
const newReadState = !isCurrentlyRead;
-
- setOptimisticArticle(newReadState);
+ onReadChange?.(articleId, newReadState);
if (isCurrentlyRead) {
await markArticleUnread({ id: articleId });
@@ -50,9 +41,8 @@ export function ArticleItem({ article }: Props) {
const handleArticleClick = async (article: Article) => {
// Open article in new tab and mark as read if it's unread
window.open(article.url, "_blank", "noreferrer");
- if (!optimisticArticle.isRead) {
- setOptimisticArticle(true);
-
+ if (!article.isRead) {
+ onReadChange?.(article.id, true);
await markArticleRead({ id: article.id });
}
};
@@ -60,38 +50,36 @@ export function ArticleItem({ article }: Props) {
return (
<div
className={`group flex items-center gap-3 rounded-lg border p-3 hover:bg-gray-50 ${
- optimisticArticle.isRead
+ article.isRead
? "border-gray-200 bg-white"
: "border-blue-200 bg-blue-50"
}`}
>
<button
type="button"
- onClick={() => handleToggleRead(article.id, optimisticArticle.isRead)}
+ onClick={() => handleToggleRead(article.id, article.isRead)}
className={`flex-shrink-0 rounded p-1 transition-colors ${
- optimisticArticle.isRead
+ article.isRead
? "text-gray-400 hover:text-gray-600"
: "text-blue-600 hover:text-blue-700"
}`}
- title={optimisticArticle.isRead ? "Mark as unread" : "Mark as read"}
+ title={article.isRead ? "Mark as unread" : "Mark as read"}
>
<FontAwesomeIcon
- icon={optimisticArticle.isRead ? faCheck : faCircle}
+ icon={article.isRead ? faCheck : faCircle}
className="w-4 h-4"
/>
</button>
<div className="flex-1 min-w-0">
<button
type="button"
- onClick={() => handleArticleClick(optimisticArticle)}
+ onClick={() => handleArticleClick(article)}
className={`text-left w-full group-hover:text-blue-600 transition-colors ${
- optimisticArticle.isRead
- ? "text-gray-700"
- : "text-gray-900 font-medium"
+ article.isRead ? "text-gray-700" : "text-gray-900 font-medium"
}`}
>
<div className="flex items-center gap-2 break-words">
- {optimisticArticle.title}
+ {article.title}
</div>
</button>
</div>
diff --git a/frontend/src/components/ArticleList.tsx b/frontend/src/components/ArticleList.tsx
index 2ea94f0..574f529 100644
--- a/frontend/src/components/ArticleList.tsx
+++ b/frontend/src/components/ArticleList.tsx
@@ -1,3 +1,4 @@
+import { useState } from "react";
import type {
GetReadArticlesQuery,
GetUnreadArticlesQuery,
@@ -9,17 +10,32 @@ interface Props {
| GetUnreadArticlesQuery["unreadArticles"]
| GetReadArticlesQuery["readArticles"]
>;
+ isReadView?: boolean;
}
-export function ArticleList({ articles }: Props) {
- if (articles.length === 0) {
+export function ArticleList({ articles, isReadView }: Props) {
+ const [hiddenArticleIds, setHiddenArticleIds] = useState<Set<string>>(
+ new Set(),
+ );
+
+ const handleArticleReadChange = (articleId: string, isRead: boolean) => {
+ if (isReadView !== isRead) {
+ setHiddenArticleIds((prev) => new Set(prev).add(articleId));
+ }
+ };
+
+ const visibleArticles = articles.filter(
+ (article) => !hiddenArticleIds.has(article.id),
+ );
+
+ if (visibleArticles.length === 0) {
return (
<div className="p-4 text-center text-gray-500">No articles found.</div>
);
}
// Group articles by feed
- const articlesByFeed = articles.reduce(
+ const articlesByFeed = visibleArticles.reduce(
(acc, article) => {
const feedId = article.feed.id;
if (!acc[feedId]) {
@@ -50,7 +66,11 @@ export function ArticleList({ articles }: Props) {
</h3>
<div className="space-y-1">
{feedArticles.map((article) => (
- <ArticleItem key={article.id} article={article} />
+ <ArticleItem
+ key={article.id}
+ article={article}
+ onReadChange={handleArticleReadChange}
+ />
))}
</div>
</div>
diff --git a/frontend/src/pages/ReadArticles.tsx b/frontend/src/pages/ReadArticles.tsx
index 44ae834..74c1f5c 100644
--- a/frontend/src/pages/ReadArticles.tsx
+++ b/frontend/src/pages/ReadArticles.tsx
@@ -26,7 +26,9 @@ export function ReadArticles() {
</p>
)}
</div>
- {data?.readArticles && <ArticleList articles={data.readArticles} />}
+ {data?.readArticles && (
+ <ArticleList articles={data.readArticles} isReadView={true} />
+ )}
</div>
);
}
diff --git a/frontend/src/pages/UnreadArticles.tsx b/frontend/src/pages/UnreadArticles.tsx
index f82d4dc..45ae0f9 100644
--- a/frontend/src/pages/UnreadArticles.tsx
+++ b/frontend/src/pages/UnreadArticles.tsx
@@ -26,7 +26,9 @@ export function UnreadArticles() {
</p>
)}
</div>
- {data?.unreadArticles && <ArticleList articles={data.unreadArticles} />}
+ {data?.unreadArticles && (
+ <ArticleList articles={data.unreadArticles} isReadView={false} />
+ )}
</div>
);
}