From 0a69e0678e140a7d369c268e8fdfe8ae7a2abc08 Mon Sep 17 00:00:00 2001
From: Pinapelz <yukais@pinapelz.com>
Date: Wed, 8 Apr 2026 09:50:50 -0700
Subject: frontedn: push unviewed items to top of view

---
 site/src/components/NewsFeed.tsx | 121 ++++++++++++++++++++++++++++-----------
 1 file changed, 88 insertions(+), 33 deletions(-)

(limited to 'site/src/components/NewsFeed.tsx')

diff --git a/site/src/components/NewsFeed.tsx b/site/src/components/NewsFeed.tsx
index ed854a0..2b81fe5 100644
--- a/site/src/components/NewsFeed.tsx
+++ b/site/src/components/NewsFeed.tsx
@@ -25,6 +25,29 @@ interface NewsFeedProps {
   newsItems: NewsData[];
 }
 
+type Sentinel = { _sentinel: "nothing-new" | "caught-up" };
+
+const VIEWED_KEY = "viewedNewsIds";
+
+const computeNewsId = (news: NewsData): string => {
+  const contentHash =
+    news.content
+      .split("")
+      .reduce(
+        (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
+        5381,
+      ) >>> 0;
+  const headlineHash =
+    (news.headline || "null")
+      .split("")
+      .reduce(
+        (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
+        5381,
+      ) >>> 0;
+  const legacyId = `${news.identifier}-${news.timestamp}-${contentHash.toString(16)}-${headlineHash.toString(16)}`;
+  return news.archive_hash || legacyId;
+};
+
 export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
   const { t } = useTranslation();
   const [showEnglish, setShowEnglish] = useState<Record<string, boolean>>({});
@@ -40,6 +63,40 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
   const pfpBaseUrl = import.meta.env.VITE_PFP_BASE_URL;
   const middlewareBase = import.meta.env.VITE_MIDDLEWARE_BASE_URL;
 
+  const [initialViewedIds] = useState<Set<string>>(() => {
+    try {
+      const raw = localStorage.getItem(VIEWED_KEY);
+      if (!raw) return new Set();
+      const parsed = JSON.parse(raw) as Array<{ id: string } | string>;
+      return new Set(parsed.map((e) => (typeof e === "string" ? e : e.id)));
+    } catch {
+      return new Set();
+    }
+  });
+
+  useEffect(() => {
+    try {
+      const raw = localStorage.getItem(VIEWED_KEY);
+      const prev: { id: string; timestamp: number }[] = raw
+        ? (JSON.parse(raw) as Array<{ id: string; timestamp?: number } | string>).map(
+            (e) => (typeof e === "string" ? { id: e, timestamp: 0 } : { id: e.id, timestamp: e.timestamp ?? 0 }),
+          )
+        : [];
+      const map = new Map(prev.map((e) => [e.id, e.timestamp]));
+      for (const news of newsItems) {
+        const id = computeNewsId(news);
+        if (!map.has(id)) map.set(id, news.timestamp);
+      }
+      const next = [...map.entries()]
+        .map(([id, timestamp]) => ({ id, timestamp }))
+        .sort((a, b) => b.timestamp - a.timestamp)
+        .slice(0, 100);
+      localStorage.setItem(VIEWED_KEY, JSON.stringify(next));
+    } catch {
+      console.error("Failed to update viewed news items");
+    }
+  }, [newsItems]);
+
   const toggleLanguage = (id: string) =>
     setShowEnglish((prev) => ({ ...prev, [id]: !prev[id] }));
   const toggleExpand = (id: string) =>
@@ -56,22 +113,7 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
   useEffect(() => {
     const initialImageIndex: Record<string, number> = {};
     newsItems.forEach((news) => {
-      const contentHash =
-        news.content
-          .split("")
-          .reduce(
-            (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
-            5381,
-          ) >>> 0;
-      const headlineHash =
-        (news.headline || "null")
-          .split("")
-          .reduce(
-            (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
-            5381,
-          ) >>> 0;
-      const newsId = `${news.identifier}-${news.timestamp}-${contentHash.toString(16)}-${headlineHash.toString(16)}`;
-      initialImageIndex[newsId] = 0;
+      initialImageIndex[computeNewsId(news)] = 0;
     });
     setCurrentImageIndex(initialImageIndex);
   }, [newsItems]);
@@ -88,29 +130,42 @@ export const NewsFeed: React.FC<NewsFeedProps> = ({ newsItems }) => {
     }
   }, [newsItems]);
 
+  const unviewed = newsItems
+    .filter((n) => !initialViewedIds.has(computeNewsId(n)))
+    .sort((a, b) => b.timestamp - a.timestamp);
+  const viewed = newsItems.filter((n) => initialViewedIds.has(computeNewsId(n)));
+  const nothingNew = unviewed.length === 0 && newsItems.length > 0;
+
+  const feed: (NewsData | Sentinel)[] = [];
+  if (nothingNew) feed.push({ _sentinel: "nothing-new" });
+  feed.push(...unviewed);
+  if (unviewed.length > 0 && viewed.length > 0) feed.push({ _sentinel: "caught-up" });
+  feed.push(...(nothingNew ? newsItems : viewed));
+
   return (
     <div className="max-w-[600px] w-full mx-auto py-8 space-y-4 font-[Zen_Maru_Gothic]">
-      {newsItems.map((news) => {
+      {feed.map((item) => {
+        if ("_sentinel" in item) {
+          return (
+            <div
+              key={item._sentinel}
+              className={`flex font-bold items-center gap-3 py-24 ${isMoe ? "text-pink-400" : "text-gray-100"}`}
+            >
+              <div className={`flex-1 h-px ${isMoe ? "bg-pink-300" : "bg-gray-700"}`} />
+              <span className="text-sm">
+                {item._sentinel === t("nothing_new") ? t("nothing_new") : t("caught_up")}
+              </span>
+              <div className={`flex-1 h-px ${isMoe ? "bg-pink-300" : "bg-gray-700"}`} />
+            </div>
+          );
+        }
+
+        const news = item;
         const date = new Date(news.timestamp * 1000).toLocaleDateString(
           "ja-JP",
           { year: "numeric", month: "2-digit", day: "2-digit" },
         );
-        const contentHash =
-          news.content
-            .split("")
-            .reduce(
-              (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
-              5381,
-            ) >>> 0;
-        const headlineHash =
-          (news.headline || "null")
-            .split("")
-            .reduce(
-              (hash, char) => (hash << 5) + hash + char.charCodeAt(0),
-              5381,
-            ) >>> 0;
-        const legacyId = `${news.identifier}-${news.timestamp}-${contentHash.toString(16)}-${headlineHash.toString(16)}`;
-        const newsId = news.archive_hash || legacyId;
+        const newsId = computeNewsId(news);
         const isEnglish = showEnglish[newsId];
         const hasTranslation = news.en_headline || news.en_content;
         const displayHeadline =
-- 
cgit v1.2.3

