/ src / app.jsx
app.jsx
  1  import './app.css';
  2  
  3  import {
  4    useEffect,
  5    useLayoutEffect,
  6    useMemo,
  7    useRef,
  8    useState,
  9  } from 'preact/hooks';
 10  import { matchPath, Route, Routes, useLocation } from 'react-router-dom';
 11  import 'swiped-events';
 12  import { useSnapshot } from 'valtio';
 13  
 14  import BackgroundService from './components/background-service';
 15  import ComposeButton from './components/compose-button';
 16  import { ICONS } from './components/icon';
 17  import KeyboardShortcutsHelp from './components/keyboard-shortcuts-help';
 18  import Loader from './components/loader';
 19  import Modals from './components/modals';
 20  import NotificationService from './components/notification-service';
 21  import SearchCommand from './components/search-command';
 22  import Shortcuts from './components/shortcuts';
 23  import NotFound from './pages/404';
 24  import AccountStatuses from './pages/account-statuses';
 25  import Bookmarks from './pages/bookmarks';
 26  import Favourites from './pages/favourites';
 27  import FollowedHashtags from './pages/followed-hashtags';
 28  import Following from './pages/following';
 29  import Hashtag from './pages/hashtag';
 30  import Home from './pages/home';
 31  import Filters from './pages/filters';
 32  import HttpRoute from './pages/http-route';
 33  import List from './pages/list';
 34  import Lists from './pages/lists';
 35  import Login from './pages/login';
 36  import Mentions from './pages/mentions';
 37  import Notifications from './pages/notifications';
 38  import Public from './pages/public';
 39  import Search from './pages/search';
 40  import StatusRoute from './pages/status-route';
 41  import Trending from './pages/trending';
 42  import ForYou from './pages/forYou';
 43  import Topics from './pages/topics';
 44  import Welcome from './pages/welcome';
 45  import {
 46    api,
 47    initAccount,
 48    initClient,
 49    initInstance,
 50    initPreferences,
 51  } from './utils/api';
 52  import { getAccessToken } from './utils/auth';
 53  import focusDeck from './utils/focus-deck';
 54  import states, { initStates } from './utils/states';
 55  import store from './utils/store';
 56  import { getCurrentAccount } from './utils/store-utils';
 57  import './utils/toast-alert';
 58  import Link from './components/link';
 59  import Icon from './components/icon';
 60  import AsyncText from './components/AsyncText';
 61  import ImportFriends from './pages/importFriends';
 62  import ImportTwitter from './pages/importTwitter';
 63  import Modal from './components/modal';
 64  import ListManageMembers from './pages/list';
 65  // import {formattedShortcuts} from './utils/shortcuts';
 66  import {subscribeToProvocWordDict} from './utils/vibe-tag';
 67  
 68  window.__STATES__ = states;
 69  
 70  // Preload icons
 71  // There's probably a better way to do this
 72  // Related: https://github.com/vitejs/vite/issues/10600
 73  setTimeout(() => {
 74    for (const icon in ICONS) {
 75      if (Array.isArray(ICONS[icon])) {
 76        ICONS[icon][0]?.();
 77      } else {
 78        ICONS[icon]?.();
 79      }
 80    }
 81  }, 5000);
 82  
 83  function App() {
 84    subscribeToProvocWordDict();
 85    const snapStates = useSnapshot(states);
 86    const [isLoggedIn, setIsLoggedIn] = useState(false);
 87    const [uiState, setUIState] = useState('loading');
 88    store.local.set('provocContentWordDict', '');
 89  
 90    useLayoutEffect(() => {
 91      const theme = store.local.get('theme');
 92      if (theme) {
 93        document.documentElement.classList.add(`is-${theme}`);
 94        document
 95          .querySelector('meta[name="color-scheme"]')
 96          .setAttribute('content', theme === 'auto' ? 'dark light' : theme);
 97      }
 98      const textSize = store.local.get('textSize');
 99      if (textSize) {
100        document.documentElement.style.setProperty(
101          '--text-size',
102          `${textSize}px`,
103        );
104      }
105    }, []);
106  
107    useEffect(() => {
108      const instanceURL = store.local.get('instanceURL');
109      const code = decodeURIComponent(
110        (window.location.search.match(/code=([^&]+)/) || [, ''])[1],
111      );
112  
113      if (code) {
114        console.log({ code });
115        // Clear the code from the URL
116        window.history.replaceState({}, document.title, location.pathname || '/');
117  
118        const clientID = store.session.get('clientID');
119        const clientSecret = store.session.get('clientSecret');
120        const vapidKey = store.session.get('vapidKey');
121  
122        (async () => {
123          setUIState('loading');
124          const { access_token: accessToken } = await getAccessToken({
125            instanceURL,
126            client_id: clientID,
127            client_secret: clientSecret,
128            code,
129          });
130  
131          const client = initClient({ instance: instanceURL, accessToken });
132          await Promise.allSettled([
133            initInstance(client, instanceURL),
134            initAccount(client, instanceURL, accessToken, vapidKey),
135          ]);
136          initStates();
137          initPreferences(client);
138  
139          setIsLoggedIn(true);
140          setUIState('default');
141        })();
142      } else {
143        window.__IGNORE_GET_ACCOUNT_ERROR__ = true;
144        const account = getCurrentAccount();
145        const myCurrentInstance = api().instance;
146        store.local.set('instanceURL', myCurrentInstance);
147        const { instance } = api();
148        const { masto } = api({ instance });
149  
150        if (account) {
151          store.session.set('currentAccount', account.info.id);
152          const { client } = api({ account });
153          const { instance } = client;
154  
155          // console.log('masto', masto);
156          initStates();
157          initPreferences(client);
158          setUIState('loading');
159          (async () => {
160            try {
161              await initInstance(client, instance);
162            } catch (e) {
163            } finally {
164              setIsLoggedIn(true);
165              setUIState('default');
166            }
167          })();
168        } else {
169          setUIState('default');
170        }
171      }
172    }, []);
173  
174    let location = useLocation();
175    states.currentLocation = location.pathname;
176  
177    useEffect(focusDeck, [location, isLoggedIn]);
178  
179    const prevLocation = snapStates.prevLocation;
180    const backgroundLocation = useRef(prevLocation || null);
181    const isModalPage = useMemo(() => {
182      return (
183        matchPath('/:instance/s/:id', location.pathname) ||
184        matchPath('/s/:id', location.pathname)
185      );
186    }, [location.pathname, matchPath]);
187    if (isModalPage) {
188      if (!backgroundLocation.current) backgroundLocation.current = prevLocation;
189    } else {
190      backgroundLocation.current = null;
191    }
192    console.debug({
193      backgroundLocation: backgroundLocation.current,
194      location,
195    });
196  
197    if (/\/https?:/.test(location.pathname)) {
198      return <HttpRoute />;
199    }
200  
201    const nonRootLocation = useMemo(() => {
202      const { pathname } = location;
203      return !/^\/(login|welcome)/.test(pathname);
204    }, [location]);
205  
206    // Change #app dataset based on snapStates.settings.shortcutsViewMode
207    useEffect(() => {
208      const $app = document.getElementById('app');
209      if ($app) {
210        $app.dataset.shortcutsViewMode = snapStates.shortcuts?.length
211          ? snapStates.settings.shortcutsViewMode
212          : '';
213      }
214    }, [snapStates.shortcuts, snapStates.settings.shortcutsViewMode]);
215  
216    // Add/Remove cloak class to body
217    useEffect(() => {
218      const $body = document.body;
219      $body.classList.toggle('cloak', snapStates.settings.cloakMode);
220    }, [snapStates.settings.cloakMode]);
221  
222    const instanceUrl = store.local.get('instanceURL');
223  
224    let trending = {
225      id: 'trending',
226      title: 'Trending',
227      subtitle: '',
228      path: `/mastodon.social/trending`,
229      icon: 'chart',
230    }
231  
232    if (instanceUrl?.indexOf('skybridge.fly.dev')) {
233      trending = {
234        id: 'trending',
235        title: 'Trending',
236        subtitle: '',
237        path: `/l/1860062187893555200`,
238        icon: 'chart',
239      }
240    }
241  
242    const formattedShortcuts = [
243      {
244        icon: "home",
245        id: "home",
246        path: "/",
247        subtitle: undefined,
248        title: "Home"
249      },
250      trending,
251      {
252        id: 'foryou',
253        title: 'For You',
254        subtitle: '',
255        path: `/foryou`,
256        icon: 'algorithm',
257      },
258      {
259        id: 'search',
260        title: 'Search',
261        path: '/search',
262        icon: 'search',
263      },
264      {
265        icon: "notification",
266        id: "notifications",
267        path: "/notifications",
268        subtitle: undefined,
269        title: "Notifications"
270      },
271    ]
272  
273    return (
274      <>
275        <Routes location={nonRootLocation || location}>
276          <Route
277            path="/"
278            element={
279              isLoggedIn ? (
280                <Home />
281              ) : uiState === 'loading' ? (
282                <Loader id="loader-root" />
283              ) : (
284                <Welcome />
285              )
286            }
287          />
288          <Route path="/login" element={<Login />} />
289          <Route path="/welcome" element={<Welcome />} />
290        </Routes>
291        <Routes location={backgroundLocation.current || location}>
292          {isLoggedIn && (
293            <Route path="/notifications" element={<Notifications />} />
294          )}
295          {isLoggedIn && <Route path="/mentions" element={<Mentions />} />}
296          {isLoggedIn && <Route path="/following" element={<Following />} />}
297          {isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
298          {isLoggedIn && <Route path="/f" element={<Favourites />} />}
299          {isLoggedIn && (
300            <Route path="/l">
301              <Route index element={<Lists />} />
302              <Route path=":id" element={<List />} />
303            </Route>
304          )}
305          {isLoggedIn && <Route path="/importfriends" element={<ImportFriends />} />}
306          {isLoggedIn && <Route path="/importtwitter" element={<ImportTwitter />} />}
307          {isLoggedIn && <Route path="/fh" element={<FollowedHashtags />} />}
308          {isLoggedIn && <Route path="/ft" element={<Filters />} />}
309          <Route path="/:instance?/t/:hashtag" element={<Hashtag />} />
310          <Route path="/:instance?/a/:id" element={<AccountStatuses />} />
311          <Route path="/:instance?/p">
312            <Route index element={<Public />} />
313            <Route path="l" element={<Public local />} />
314          </Route>
315          <Route path="/:instance?/trending" element={<Trending />} />
316          <Route path="/foryou" element={<ForYou />} />
317          {isLoggedIn && <Route path="/topics" element={<Topics />} />}
318          <Route path="/:instance?/search" element={<Search />} />
319          {/* <Route path="/:anything" element={<NotFound />} /> */}
320        </Routes>
321        {uiState === 'default' && (
322          <Routes>
323            <Route path="/:instance?/s/:id" element={<StatusRoute />} />
324          </Routes>
325        )}
326              {/* {true && (
327          <Modal
328            class="light"
329            onClick={(e) => {
330              if (e.target === e.currentTarget) {
331                setShowManageMembersModal(false);
332              }
333            }}
334          >
335            <ListManageMembers
336              listID={"8133"}
337              onClose={() => setShowManageMembersModal(false)}
338            />
339          </Modal>
340        )} */}
341        {isLoggedIn && <ComposeButton />}
342        {isLoggedIn &&
343          !snapStates.settings.shortcutsColumnsMode &&
344          snapStates.settings.shortcutsViewMode !== 'multi-column' && (
345            <Shortcuts />
346          )}
347          {isLoggedIn && <nav class="tab-bar">
348            <ul>
349              {formattedShortcuts.map(
350                ({ id, path, title, subtitle, icon }, i) => {
351                  return (
352                    <li key={i + title}>
353                      <Link
354                        class={title === 'Notifications' && snapStates.notificationsShowNew ? 'has-badge-tab-bar' : ''}
355                        to={path}
356                        onClick={(e) => {
357                          if (e.target.classList.contains('is-active')) {
358                            e.preventDefault();
359                            const page = document.getElementById(`${id}-page`);
360                            console.log(id, page);
361                            if (page) {
362                              page.scrollTop = 0;
363                              const updatesButton =
364                                page.querySelector('.updates-button');
365                              if (updatesButton) {
366                                updatesButton.click();
367                              }
368                            }
369                          }
370                        }}
371                      >
372                        <Icon icon={icon} size="xl" alt={title} />
373                        <span>
374                          <AsyncText>{title}</AsyncText>
375                          {subtitle && (
376                            <>
377                              <br />
378                              <small>{subtitle}</small>
379                            </>
380                          )}
381                        </span>
382                      </Link>
383                    </li>
384                  );
385                },
386              )}
387            </ul>
388          </nav>}
389  
390        <Modals />
391        {isLoggedIn && <NotificationService />}
392        <BackgroundService isLoggedIn={isLoggedIn} />
393        {uiState !== 'loading' && <SearchCommand onClose={focusDeck} />}
394        <KeyboardShortcutsHelp />
395      </>
396    );
397  }
398  
399  export { App };