From 913a352deefe67218556a457100c78ee624f5eca Mon Sep 17 00:00:00 2001
From: Lim Chee Aun <cheeaun@gmail.com>
Date: Thu, 6 Apr 2023 01:14:38 +0800
Subject: [PATCH] Add Trending page

---
 src/app.jsx             |   2 +
 src/components/icon.jsx |   1 +
 src/components/menu.jsx |   3 +
 src/pages/trending.jsx  | 127 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 133 insertions(+)
 create mode 100644 src/pages/trending.jsx

diff --git a/src/app.jsx b/src/app.jsx
index 8e4001aa..391e2e45 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -41,6 +41,7 @@ import Public from './pages/public';
 import Search from './pages/search';
 import Settings from './pages/settings';
 import Status from './pages/status';
+import Trending from './pages/trending';
 import Welcome from './pages/welcome';
 import {
   api,
@@ -238,6 +239,7 @@ function App() {
           <Route index element={<Public />} />
           <Route path="l" element={<Public local />} />
         </Route>
+        <Route path="/:instance?/trending" element={<Trending />} />
         <Route path="/:instance?/search" element={<Search />} />
         {/* <Route path="/:anything" element={<NotFound />} /> */}
       </Routes>
diff --git a/src/components/icon.jsx b/src/components/icon.jsx
index afc6e906..0d37deb6 100644
--- a/src/components/icon.jsx
+++ b/src/components/icon.jsx
@@ -75,6 +75,7 @@ const ICONS = {
   refresh: 'mingcute:refresh-2-line',
   emoji2: 'mingcute:emoji-2-line',
   filter: 'mingcute:filter-2-line',
+  chart: 'mingcute:chart-line-line',
 };
 
 const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
diff --git a/src/components/menu.jsx b/src/components/menu.jsx
index 7e36ca83..363e5219 100644
--- a/src/components/menu.jsx
+++ b/src/components/menu.jsx
@@ -137,6 +137,9 @@ function NavMenu(props) {
       <MenuLink to={`/${instance}/p`}>
         <Icon icon="earth" size="l" /> <span>Federated</span>
       </MenuLink>
+      <MenuLink to={`/${instance}/trending`}>
+        <Icon icon="chart" size="l" /> <span>Trending</span>
+      </MenuLink>
       {authenticated && (
         <>
           <MenuDivider />
diff --git a/src/pages/trending.jsx b/src/pages/trending.jsx
new file mode 100644
index 00000000..c92a7e11
--- /dev/null
+++ b/src/pages/trending.jsx
@@ -0,0 +1,127 @@
+import { Menu, MenuItem } from '@szhsin/react-menu';
+import { useRef } from 'preact/hooks';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useSnapshot } from 'valtio';
+
+import Icon from '../components/icon';
+import Timeline from '../components/timeline';
+import { api } from '../utils/api';
+import { filteredItems } from '../utils/filters';
+import states from '../utils/states';
+import { saveStatus } from '../utils/states';
+import useTitle from '../utils/useTitle';
+
+const LIMIT = 20;
+
+function Trending(props) {
+  const snapStates = useSnapshot(states);
+  const params = useParams();
+  const { masto, instance } = api({
+    instance: props?.instance || params.instance,
+  });
+  const title = `Trending (${instance})`;
+  useTitle(title, `/:instance?/trending`);
+  const navigate = useNavigate();
+  const latestItem = useRef();
+
+  const trendIterator = useRef();
+  async function fetchTrend(firstLoad) {
+    if (firstLoad || !trendIterator.current) {
+      trendIterator.current = masto.v1.trends.listStatuses({
+        limit: LIMIT,
+      });
+    }
+    const results = await trendIterator.current.next();
+    let { value } = results;
+    if (value?.length) {
+      if (firstLoad) {
+        latestItem.current = value[0].id;
+      }
+
+      value = filteredItems(value, 'public'); // Might not work here
+      value.forEach((item) => {
+        saveStatus(item, instance);
+      });
+    }
+    return results;
+  }
+
+  async function checkForUpdates() {
+    try {
+      const results = await masto.v1.trends
+        .listStatuses({
+          limit: 1,
+          since_id: latestItem.current,
+        })
+        .next();
+      let { value } = results;
+      value = filteredItems(value, 'public');
+      if (value?.length) {
+        return true;
+      }
+      return false;
+    } catch (e) {
+      return false;
+    }
+  }
+
+  return (
+    <Timeline
+      key={instance}
+      title={title}
+      titleComponent={
+        <h1 class="header-account">
+          <b>Trending</b>
+          <div>{instance}</div>
+        </h1>
+      }
+      id="trending"
+      instance={instance}
+      emptyText="No trending posts."
+      errorText="Unable to load posts"
+      fetchItems={fetchTrend}
+      checkForUpdates={checkForUpdates}
+      useItemID
+      headerStart={<></>}
+      boostsCarousel={snapStates.settings.boostsCarousel}
+      allowFilters
+      headerEnd={
+        <Menu
+          portal={{
+            target: document.body,
+          }}
+          // setDownOverflow
+          overflow="auto"
+          viewScroll="close"
+          position="anchor"
+          boundingBoxPadding="8 8 8 8"
+          menuButton={
+            <button type="button" class="plain">
+              <Icon icon="more" size="l" />
+            </button>
+          }
+        >
+          <MenuItem
+            onClick={() => {
+              let newInstance = prompt(
+                'Enter a new instance e.g. "mastodon.social"',
+              );
+              if (!/\./.test(newInstance)) {
+                if (newInstance) alert('Invalid instance');
+                return;
+              }
+              if (newInstance) {
+                newInstance = newInstance.toLowerCase().trim();
+                navigate(`/${newInstance}/trending`);
+              }
+            }}
+          >
+            <Icon icon="bus" /> <span>Go to another instance…</span>
+          </MenuItem>
+        </Menu>
+      }
+    />
+  );
+}
+
+export default Trending;