From 1357c1b2bd134689af2da734de82275b47b44f94 Mon Sep 17 00:00:00 2001
From: Lim Chee Aun <cheeaun@gmail.com>
Date: Mon, 6 Feb 2023 16:35:03 +0800
Subject: [PATCH] Fix more edge cases after breaking changes

---
 src/app.jsx                    | 18 ++++++++--------
 src/components/account.jsx     |  4 ++--
 src/components/status.jsx      | 39 +++++++++++++++++-----------------
 src/pages/account-statuses.jsx | 10 ++++++---
 src/pages/hashtags.jsx         | 10 +++++----
 src/pages/home.jsx             |  4 ++--
 src/pages/lists.jsx            |  2 +-
 src/pages/public.jsx           |  6 +++---
 src/pages/status.jsx           | 31 +++++++++++++++++----------
 src/utils/states.js            | 34 ++++++++++++++++++++++-------
 10 files changed, 96 insertions(+), 62 deletions(-)

diff --git a/src/app.jsx b/src/app.jsx
index 8a7ec4d0..9255ee0c 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -37,7 +37,7 @@ import Status from './pages/status';
 import Welcome from './pages/welcome';
 import { api, initAccount, initClient, initInstance } from './utils/api';
 import { getAccessToken } from './utils/auth';
-import states, { saveStatus } from './utils/states';
+import states, { getStatus, saveStatus } from './utils/states';
 import store from './utils/store';
 import { getCurrentAccount } from './utils/store-utils';
 
@@ -330,7 +330,7 @@ function App() {
 
 let ws;
 async function startStream() {
-  const { masto } = api();
+  const { masto, instance } = api();
   if (
     ws &&
     (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
@@ -361,17 +361,17 @@ async function startStream() {
       }
     }
 
-    saveStatus(status);
+    saveStatus(status, instance);
   }, 5000);
   stream.on('update', handleNewStatus);
   stream.on('status.update', (status) => {
     console.log('STATUS.UPDATE', status);
-    saveStatus(status);
+    saveStatus(status, instance);
   });
   stream.on('delete', (statusID) => {
     console.log('DELETE', statusID);
     // delete states.statuses[statusID];
-    const s = states.statuses[statusID];
+    const s = getStatus(statusID);
     if (s) s._deleted = true;
   });
   stream.on('notification', (notification) => {
@@ -385,7 +385,7 @@ async function startStream() {
       states.notificationsNew.unshift(notification);
     }
 
-    saveStatus(notification.status, { override: false });
+    saveStatus(notification.status, instance, { override: false });
   });
 
   stream.ws.onclose = () => {
@@ -405,7 +405,7 @@ async function startStream() {
 
 let lastHidden;
 function startVisibility() {
-  const { masto } = api();
+  const { masto, instance } = api();
   const handleVisible = (visible) => {
     if (!visible) {
       const timestamp = Date.now();
@@ -438,7 +438,7 @@ function startVisibility() {
                 // do nothing
               } else {
                 states.homeNew = newStatuses.map((status) => {
-                  saveStatus(status);
+                  saveStatus(status, instance);
                   return {
                     id: status.id,
                     reblog: status.reblog?.id,
@@ -461,7 +461,7 @@ function startVisibility() {
                 states.notificationsNew.unshift(notification);
               }
 
-              saveStatus(notification.status, { override: false });
+              saveStatus(notification.status, instance, { override: false });
             }
           } catch (e) {
             // Silently fail
diff --git a/src/components/account.jsx b/src/components/account.jsx
index 0ade7b4d..6d244064 100644
--- a/src/components/account.jsx
+++ b/src/components/account.jsx
@@ -15,8 +15,8 @@ import Icon from './icon';
 import Link from './link';
 import NameText from './name-text';
 
-function Account({ account, instance, onClose }) {
-  const { masto, authenticated } = api({ instance });
+function Account({ account, instance: propInstance, onClose }) {
+  const { masto, instance, authenticated } = api({ instance: propInstance });
   const [uiState, setUIState] = useState('default');
   const isString = typeof account === 'string';
   const [info, setInfo] = useState(isString ? null : account);
diff --git a/src/components/status.jsx b/src/components/status.jsx
index 9eaa7fd3..0b0e196e 100644
--- a/src/components/status.jsx
+++ b/src/components/status.jsx
@@ -16,7 +16,7 @@ import enhanceContent from '../utils/enhance-content';
 import handleContentLinks from '../utils/handle-content-links';
 import htmlContentLength from '../utils/html-content-length';
 import shortenNumber from '../utils/shorten-number';
-import states, { saveStatus } from '../utils/states';
+import states, { saveStatus, statusKey } from '../utils/states';
 import store from '../utils/store';
 import visibilityIconsMap from '../utils/visibility-icons-map';
 
@@ -38,7 +38,7 @@ const memFetchAccount = mem(fetchAccount);
 function Status({
   statusID,
   status,
-  instance,
+  instance: propInstance,
   withinContext,
   size = 'm',
   skeleton,
@@ -59,11 +59,12 @@ function Status({
       </div>
     );
   }
-  const { masto, authenticated } = api({ instance });
+  const { masto, instance, authenticated } = api({ instance: propInstance });
 
+  const sKey = statusKey(statusID, instance);
   const snapStates = useSnapshot(states);
   if (!status) {
-    status = snapStates.statuses[statusID];
+    status = snapStates.statuses[sKey];
   }
   if (!status) {
     return null;
@@ -384,13 +385,13 @@ function Status({
               poll={poll}
               readOnly={readOnly || !authenticated}
               onUpdate={(newPoll) => {
-                states.statuses[id].poll = newPoll;
+                states.statuses[sKey].poll = newPoll;
               }}
               refresh={() => {
                 return masto.v1.polls
                   .fetch(poll.id)
                   .then((pollResponse) => {
-                    states.statuses[id].poll = pollResponse;
+                    states.statuses[sKey].poll = pollResponse;
                   })
                   .catch((e) => {}); // Silently fail
               }}
@@ -400,7 +401,7 @@ function Status({
                     choices,
                   })
                   .then((pollResponse) => {
-                    states.statuses[id].poll = pollResponse;
+                    states.statuses[sKey].poll = pollResponse;
                   })
                   .catch((e) => {}); // Silently fail
               }}
@@ -544,7 +545,7 @@ function Status({
                           }
                         }
                         // Optimistic
-                        states.statuses[id] = {
+                        states.statuses[sKey] = {
                           ...status,
                           reblogged: !reblogged,
                           reblogsCount: reblogsCount + (reblogged ? -1 : 1),
@@ -553,15 +554,15 @@ function Status({
                           const newStatus = await masto.v1.statuses.unreblog(
                             id,
                           );
-                          saveStatus(newStatus);
+                          saveStatus(newStatus, instance);
                         } else {
                           const newStatus = await masto.v1.statuses.reblog(id);
-                          saveStatus(newStatus);
+                          saveStatus(newStatus, instance);
                         }
                       } catch (e) {
                         console.error(e);
                         // Revert optimistism
-                        states.statuses[id] = status;
+                        states.statuses[sKey] = status;
                       }
                     }}
                   />
@@ -581,7 +582,7 @@ function Status({
                     }
                     try {
                       // Optimistic
-                      states.statuses[statusID] = {
+                      states.statuses[sKey] = {
                         ...status,
                         favourited: !favourited,
                         favouritesCount:
@@ -591,15 +592,15 @@ function Status({
                         const newStatus = await masto.v1.statuses.unfavourite(
                           id,
                         );
-                        saveStatus(newStatus);
+                        saveStatus(newStatus, instance);
                       } else {
                         const newStatus = await masto.v1.statuses.favourite(id);
-                        saveStatus(newStatus);
+                        saveStatus(newStatus, instance);
                       }
                     } catch (e) {
                       console.error(e);
                       // Revert optimistism
-                      states.statuses[statusID] = status;
+                      states.statuses[sKey] = status;
                     }
                   }}
                 />
@@ -617,7 +618,7 @@ function Status({
                     }
                     try {
                       // Optimistic
-                      states.statuses[statusID] = {
+                      states.statuses[sKey] = {
                         ...status,
                         bookmarked: !bookmarked,
                       };
@@ -625,15 +626,15 @@ function Status({
                         const newStatus = await masto.v1.statuses.unbookmark(
                           id,
                         );
-                        saveStatus(newStatus);
+                        saveStatus(newStatus, instance);
                       } else {
                         const newStatus = await masto.v1.statuses.bookmark(id);
-                        saveStatus(newStatus);
+                        saveStatus(newStatus, instance);
                       }
                     } catch (e) {
                       console.error(e);
                       // Revert optimistism
-                      states.statuses[statusID] = status;
+                      states.statuses[sKey] = status;
                     }
                   }}
                 />
diff --git a/src/pages/account-statuses.jsx b/src/pages/account-statuses.jsx
index f1ab28cf..2238250c 100644
--- a/src/pages/account-statuses.jsx
+++ b/src/pages/account-statuses.jsx
@@ -12,8 +12,8 @@ const LIMIT = 20;
 
 function AccountStatuses() {
   const snapStates = useSnapshot(states);
-  const { id, instance } = useParams();
-  const { masto } = api({ instance });
+  const { id, ...params } = useParams();
+  const { masto, instance } = api({ instance: params.instance });
   const accountStatusesIterator = useRef();
   async function fetchAccountStatuses(firstLoad) {
     if (firstLoad || !accountStatusesIterator.current) {
@@ -25,7 +25,10 @@ function AccountStatuses() {
   }
 
   const [account, setAccount] = useState({});
-  useTitle(`${account?.acct ? '@' + account.acct : 'Posts'}`, '/a/:id');
+  useTitle(
+    `${account?.acct ? '@' + account.acct : 'Posts'}`,
+    '/a/:instance?/:id',
+  );
   useEffect(() => {
     (async () => {
       try {
@@ -65,6 +68,7 @@ function AccountStatuses() {
         </h1>
       }
       id="account_statuses"
+      instance={instance}
       emptyText="Nothing to see here yet."
       errorText="Unable to load statuses"
       fetchItems={fetchAccountStatuses}
diff --git a/src/pages/hashtags.jsx b/src/pages/hashtags.jsx
index 075efb26..31d48800 100644
--- a/src/pages/hashtags.jsx
+++ b/src/pages/hashtags.jsx
@@ -8,9 +8,10 @@ import useTitle from '../utils/useTitle';
 const LIMIT = 20;
 
 function Hashtags() {
-  const { hashtag, instance } = useParams();
-  useTitle(`#${hashtag}`, `/t/${hashtag}`);
-  const { masto } = api({ instance });
+  let { hashtag, ...params } = useParams();
+  const { masto, instance } = api({ instance: params.instance });
+  const title = instance ? `#${hashtag} on ${instance}` : `#${hashtag}`;
+  useTitle(title, `/t/:instance?/:hashtag`);
   const hashtagsIterator = useRef();
   async function fetchHashtags(firstLoad) {
     if (firstLoad || !hashtagsIterator.current) {
@@ -24,7 +25,7 @@ function Hashtags() {
   return (
     <Timeline
       key={hashtag}
-      title={instance ? `#${hashtag} on ${instance}` : `#${hashtag}`}
+      title={title}
       titleComponent={
         !!instance && (
           <h1 class="header-account">
@@ -34,6 +35,7 @@ function Hashtags() {
         )
       }
       id="hashtags"
+      instance={instance}
       emptyText="No one has posted anything with this tag yet."
       errorText="Unable to load posts with this tag"
       fetchItems={fetchHashtags}
diff --git a/src/pages/home.jsx b/src/pages/home.jsx
index e2b6f835..4f332bcc 100644
--- a/src/pages/home.jsx
+++ b/src/pages/home.jsx
@@ -19,7 +19,7 @@ const LIMIT = 20;
 
 function Home({ hidden }) {
   useTitle('Home', '/');
-  const { masto } = api();
+  const { masto, instance } = api();
   const snapStates = useSnapshot(states);
   const isHomeLocation = snapStates.currentLocation === '/';
   const [uiState, setUIState] = useState('default');
@@ -45,7 +45,7 @@ function Home({ hidden }) {
         return bDate - aDate;
       });
       const homeValues = allStatuses.value.map((status) => {
-        saveStatus(status);
+        saveStatus(status, instance);
         return {
           id: status.id,
           reblog: status.reblog?.id,
diff --git a/src/pages/lists.jsx b/src/pages/lists.jsx
index 81b641ff..3bfcb2b3 100644
--- a/src/pages/lists.jsx
+++ b/src/pages/lists.jsx
@@ -21,7 +21,7 @@ function Lists() {
   }
 
   const [title, setTitle] = useState(`List ${id}`);
-  useTitle(title, `/l/${id}`);
+  useTitle(title, `/l/:id`);
   useEffect(() => {
     (async () => {
       try {
diff --git a/src/pages/public.jsx b/src/pages/public.jsx
index 0534ff43..a1e03a38 100644
--- a/src/pages/public.jsx
+++ b/src/pages/public.jsx
@@ -10,10 +10,10 @@ const LIMIT = 20;
 
 function Public() {
   const isLocal = !!useMatch('/p/l/:instance');
-  const { instance } = useParams();
-  const { masto } = api({ instance });
+  const params = useParams();
+  const { masto, instance } = api({ instance: params.instance });
   const title = `${instance} (${isLocal ? 'local' : 'federated'})`;
-  useTitle(title, `/p/${instance}`);
+  useTitle(title, `/p/l?/:instance`);
 
   const publicIterator = useRef();
   async function fetchPublic(firstLoad) {
diff --git a/src/pages/status.jsx b/src/pages/status.jsx
index 11d32051..d8f750e1 100644
--- a/src/pages/status.jsx
+++ b/src/pages/status.jsx
@@ -20,7 +20,11 @@ import Status from '../components/status';
 import { api } from '../utils/api';
 import htmlContentLength from '../utils/html-content-length';
 import shortenNumber from '../utils/shorten-number';
-import states, { saveStatus, threadifyStatus } from '../utils/states';
+import states, {
+  saveStatus,
+  statusKey,
+  threadifyStatus,
+} from '../utils/states';
 import { getCurrentAccount } from '../utils/store-utils';
 import useScroll from '../utils/useScroll';
 import useTitle from '../utils/useTitle';
@@ -35,13 +39,14 @@ function resetScrollPosition(id) {
 }
 
 function StatusPage() {
-  const { id, instance } = useParams();
-  const { masto } = api({ instance });
+  const { id, ...params } = useParams();
+  const { masto, instance } = api({ instance: params.instance });
   const navigate = useNavigate();
   const snapStates = useSnapshot(states);
   const [statuses, setStatuses] = useState([]);
   const [uiState, setUIState] = useState('default');
   const heroStatusRef = useRef();
+  const sKey = statusKey(id, instance);
 
   const scrollableRef = useRef();
   useEffect(() => {
@@ -76,7 +81,7 @@ function StatusPage() {
     if (cachedStatuses) {
       // Case 1: It's cached, let's restore them to make it snappy
       const reallyCachedStatuses = cachedStatuses.filter(
-        (s) => states.statuses[s.id],
+        (s) => states.statuses[sKey],
         // Some are not cached in the global state, so we need to filter them out
       );
       setStatuses(reallyCachedStatuses);
@@ -102,14 +107,14 @@ function StatusPage() {
         retries: 8,
       });
 
-      const hasStatus = !!snapStates.statuses[id];
-      let heroStatus = snapStates.statuses[id];
+      const hasStatus = !!snapStates.statuses[sKey];
+      let heroStatus = snapStates.statuses[sKey];
       if (hasStatus) {
         console.debug('Hero status is cached');
       } else {
         try {
           heroStatus = await heroFetch();
-          saveStatus(heroStatus);
+          saveStatus(heroStatus, instance);
           // Give time for context to appear
           await new Promise((resolve) => {
             setTimeout(resolve, 100);
@@ -126,11 +131,15 @@ function StatusPage() {
         const { ancestors, descendants } = context;
 
         ancestors.forEach((status) => {
-          states.statuses[status.id] = status;
+          saveStatus(status, instance, {
+            skipThreading: true,
+          });
         });
         const nestedDescendants = [];
         descendants.forEach((status) => {
-          states.statuses[status.id] = status;
+          saveStatus(status, instance, {
+            skipThreading: true,
+          });
           if (status.inReplyToAccountId === status.account.id) {
             // If replying to self, it's part of the thread, level 1
             nestedDescendants.push(status);
@@ -201,7 +210,7 @@ function StatusPage() {
         // Let's threadify this one
         // Note that all non-hero statuses will trigger saveStatus which will threadify them too
         // By right, at this point, all descendant statuses should be cached
-        threadifyStatus(heroStatus);
+        threadifyStatus(heroStatus, instance);
       } catch (e) {
         console.error(e);
         setUIState('error');
@@ -279,7 +288,7 @@ function StatusPage() {
     };
   }, []);
 
-  const heroStatus = snapStates.statuses[id];
+  const heroStatus = snapStates.statuses[sKey];
   const heroDisplayName = useMemo(() => {
     // Remove shortcodes from display name
     if (!heroStatus) return '';
diff --git a/src/utils/states.js b/src/utils/states.js
index 0b7eb983..d25056a1 100644
--- a/src/utils/states.js
+++ b/src/utils/states.js
@@ -53,22 +53,40 @@ export function hideAllModals() {
   states.showMediaModal = false;
 }
 
-export function saveStatus(status, opts) {
+export function statusKey(id, instance) {
+  return instance ? `${instance}/${id}` : id;
+}
+
+export function getStatus(statusID, instance) {
+  if (instance) {
+    const key = statusKey(statusID, instance);
+    return states.statuses[key];
+  }
+  return states.statuses[statusID];
+}
+
+export function saveStatus(status, instance, opts) {
+  if (typeof instance === 'object') {
+    opts = instance;
+    instance = null;
+  }
   const { override, skipThreading } = Object.assign(
     { override: true, skipThreading: false },
     opts,
   );
   if (!status) return;
-  if (!override && states.statuses[status.id]) return;
-  states.statuses[status.id] = status;
+  if (!override && getStatus(status.id)) return;
+  const key = statusKey(status.id, instance);
+  states.statuses[key] = status;
   if (status.reblog) {
-    states.statuses[status.reblog.id] = status.reblog;
+    const key = statusKey(status.reblog.id, instance);
+    states.statuses[key] = status.reblog;
   }
 
   // THREAD TRAVERSER
   if (!skipThreading) {
     requestAnimationFrame(() => {
-      threadifyStatus(status);
+      threadifyStatus(status, instance);
       if (status.reblog) {
         threadifyStatus(status.reblog);
       }
@@ -76,8 +94,8 @@ export function saveStatus(status, opts) {
   }
 }
 
-export function threadifyStatus(status) {
-  const { masto } = api();
+export function threadifyStatus(status, propInstance) {
+  const { masto, instance } = api({ instance: propInstance });
   // Return all statuses in the thread, via inReplyToId, if inReplyToAccountId === account.id
   let fetchIndex = 0;
   async function traverse(status, index = 0) {
@@ -94,7 +112,7 @@ export function threadifyStatus(status) {
       if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
       await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
       prevStatus = await masto.v1.statuses.fetch(inReplyToId);
-      saveStatus(prevStatus, { skipThreading: true });
+      saveStatus(prevStatus, instance, { skipThreading: true });
     }
     // Prepend so that first status in thread will be index 0
     return [...(await traverse(prevStatus, ++index)), status];