2023-09-18 11:55:30 +02:00
import React , { useEffect , useState } from 'react' ;
2023-03-20 17:08:05 +01:00
import { Route , Routes , useLocation } from 'react-router-dom' ;
2023-02-01 13:30:32 +01:00
import { Helmet } from 'react-helmet-async' ;
2023-04-09 20:55:55 +02:00
import 'react-tooltip/dist/react-tooltip.css' ;
import 'react-toastify/dist/ReactToastify.css' ;
2023-04-08 13:27:17 +02:00
import useGeneralStore from './hooks/stores/useGeneralStore' ;
2023-04-29 18:01:19 +02:00
import { GlobalStyle } from './components/styled/GlobalStyle.styled' ;
import { Toast } from './components/styled/Toast.styled' ;
2023-05-16 16:37:50 +02:00
import All from './components/views/All' ;
import AllCatalog from './components/views/AllCatalog' ;
2023-03-19 11:49:05 +01:00
import Board from './components/views/Board' ;
import Catalog from './components/views/Catalog' ;
2023-06-27 21:06:30 +02:00
import Description from './components/views/Description' ;
2023-05-03 11:01:41 +02:00
import Home from './components/views/Home' ;
2023-03-19 11:49:05 +01:00
import NotFound from './components/views/NotFound' ;
2023-04-10 10:51:16 +02:00
import Pending from './components/views/Pending' ;
2023-06-27 21:06:30 +02:00
import Rules from './components/views/Rules' ;
2023-05-03 11:01:41 +02:00
import Subscriptions from './components/views/Subscriptions' ;
2023-05-07 17:48:38 +02:00
import SubscriptionsCatalog from './components/views/SubscriptionsCatalog' ;
2023-05-03 11:01:41 +02:00
import Thread from './components/views/Thread' ;
2023-05-24 12:15:43 +02:00
import CaptchaModal from './components/modals/CaptchaModal' ;
2023-03-22 16:23:32 +01:00
import { importAll } from './components/ImageBanner' ;
2023-04-09 20:55:55 +02:00
import preloadImages from './utils/preloadImages' ;
2023-09-18 11:55:30 +02:00
import useError from './hooks/useError' ;
import useInfo from './hooks/useInfo' ;
import useSuccess from './hooks/useSuccess' ;
2023-09-10 12:20:31 +02:00
import packageJson from '../package.json' ;
2023-09-10 15:21:03 +02:00
const commitRef = process ? . env ? . REACT_APP_COMMIT_REF || '' ;
2023-02-08 21:49:36 +01:00
2023-03-13 14:29:34 +01:00
export default function App () {
2023-09-18 11:55:30 +02:00
const {
bodyStyle ,
setBodyStyle ,
setDefaultSubplebbits ,
isCaptchaOpen ,
setIsCaptchaOpen ,
setIsSettingsOpen ,
selectedStyle ,
setSelectedStyle ,
setShowPostForm ,
setShowPostFormLink ,
} = useGeneralStore (( state ) => state );
2023-03-20 17:08:05 +01:00
2023-09-18 11:55:30 +02:00
const location = useLocation ();
const isHomeRoute = location . pathname === '/' ;
const isElectron = window . electron && window . electron . isElectron ;
2023-04-29 14:39:57 +02:00
2023-09-18 11:55:30 +02:00
const [, setNewErrorMessage ] = useError ();
const [, setNewSuccessMessage ] = useSuccess ();
const [, setNewInfoMessage ] = useInfo ();
2023-05-16 13:26:05 +02:00
2023-09-18 11:55:30 +02:00
const [ infoMessageShown , setInfoMessageShown ] = useState ( false );
2023-05-16 13:26:05 +02:00
2023-09-18 11:55:30 +02:00
useEffect (() => {
const successToast = localStorage . getItem ( 'successToast' );
const errorToast = localStorage . getItem ( 'errorToast' );
if ( successToast ) {
setNewSuccessMessage ( successToast );
localStorage . removeItem ( 'successToast' );
} else if ( errorToast ) {
setNewErrorMessage ( errorToast );
localStorage . removeItem ( 'errorToast' );
} else {
return ;
}
}, [ setNewErrorMessage , setNewSuccessMessage ]);
2023-09-10 12:20:31 +02:00
2023-09-18 11:55:30 +02:00
// check for new version
// TODO: delete this code and toast when v1.0.0 is released
useEffect (() => {
const fetchVersionInfo = async () => {
try {
const packageRes = await fetch ( 'https://raw.githubusercontent.com/plebbit/plebchan/master/package.json' , { cache : 'no-cache' });
const packageData = await packageRes . json ();
2023-09-10 15:21:03 +02:00
2023-09-18 11:55:30 +02:00
if ( packageJson . version !== packageData . version && ! infoMessageShown ) {
const newVersionInfo = isElectron
? `New version available, plebchan v ${ packageData . version } . You are using v ${ packageJson . version } . Download the latest version here: https://github.com/plebbit/plebchan/releases/latest`
: `New version available, plebchan v ${ packageData . version } . You are using v ${ packageJson . version } . Refresh to update.` ;
setNewInfoMessage ( newVersionInfo );
setInfoMessageShown ( true );
}
2023-09-10 15:21:03 +02:00
2023-09-18 11:55:30 +02:00
if ( commitRef . length > 0 ) {
const commitRes = await fetch ( 'https://api.github.com/repos/plebbit/plebchan/commits?per_page=1&sha=development' , { cache : 'no-cache' });
const commitData = await commitRes . json ();
2023-09-10 15:21:03 +02:00
2023-09-18 11:55:30 +02:00
const latestCommitHash = commitData [ 0 ]. sha ;
2023-09-10 12:20:31 +02:00
2023-09-18 11:55:30 +02:00
if ( latestCommitHash . trim () !== commitRef . trim () && ! infoMessageShown ) {
const newVersionInfo = `New dev version available, commit ${ latestCommitHash . slice ( 0 , 7 ) } . You are using commit ${ commitRef . slice ( 0 , 7 ) } . Refresh to update.` ;
setNewInfoMessage ( newVersionInfo );
setInfoMessageShown ( true );
}
}
} catch ( error ) {
console . error ( 'Failed to fetch latest version info:' , error );
}
};
2023-03-22 16:23:32 +01:00
2023-09-18 11:55:30 +02:00
fetchVersionInfo ();
}, [ setNewInfoMessage , isElectron , infoMessageShown ]);
2023-03-05 14:26:40 +01:00
2023-09-18 11:55:30 +02:00
// preload banners
useEffect (() => {
const loadImages = async () => {
const images = await importAll ( require . context ( '../public/assets' , true , /\.(png|jpe?g|svg)$/ ));
const imageUrls = images . map (( image ) => image . default );
preloadImages ( imageUrls );
};
2023-03-11 17:27:46 +01:00
2023-09-18 11:55:30 +02:00
loadImages ();
}, []);
2023-03-20 17:08:05 +01:00
2023-09-18 11:55:30 +02:00
// automatic dark mode without interefering with user's selected style
useEffect (() => {
if ( isHomeRoute ) return ;
2023-03-20 17:08:05 +01:00
2023-09-18 11:55:30 +02:00
const darkModeMediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' );
const isDarkMode = darkModeMediaQuery . matches ;
2023-03-20 17:08:05 +01:00
2023-09-18 11:55:30 +02:00
if ( isDarkMode && ! isHomeRoute ) {
setSelectedStyle ( 'Tomorrow' );
setBodyStyle ({
background : '#1d1f21 none' ,
color : '#c5c8c6' ,
fontFamily : 'Arial, Helvetica, sans-serif' ,
});
localStorage . setItem ( 'selectedStyle' , 'Tomorrow' );
}
const darkModeListener = ( e ) => {
if ( e . matches && ! isHomeRoute ) {
setSelectedStyle ( 'Tomorrow' );
setBodyStyle ({
background : '#1d1f21 none' ,
color : '#c5c8c6' ,
fontFamily : 'Arial, Helvetica, sans-serif' ,
});
localStorage . setItem ( 'selectedStyle' , 'Tomorrow' );
}
};
darkModeMediaQuery . addEventListener ( 'change' , darkModeListener );
return () => {
darkModeMediaQuery . removeEventListener ( 'change' , darkModeListener );
};
}, [ isHomeRoute , setBodyStyle , setSelectedStyle ]);
// fetch default subplebbits
useEffect (() => {
let didCancel = false ;
fetch ( 'https://raw.githubusercontent.com/plebbit/temporary-default-subplebbits/master/subplebbits.json' , { cache : 'no-cache' })
. then (( res ) => res . json ())
. then (( res ) => {
if ( ! didCancel ) {
setDefaultSubplebbits ( res );
}
});
return () => {
didCancel = true ;
};
}, [ setDefaultSubplebbits ]);
// handle nested routes
useEffect (() => {
const path = location . pathname ;
if ( path . endsWith ( '/post' )) {
setShowPostFormLink ( false );
setShowPostForm ( true );
} else {
setShowPostFormLink ( true );
setShowPostForm ( false );
}
if ( path . endsWith ( '/settings' )) {
setIsSettingsOpen ( true );
} else {
setIsSettingsOpen ( false );
}
}, [ location . pathname , setIsSettingsOpen , setShowPostForm , setShowPostFormLink ]);
return (
< div >
< Helmet >
< link rel = 'apple-touch-icon' sizes = '180x180' href = 'assets/logo/apple-touch-icon.png' />
< link rel = 'icon' type = 'image/png' sizes = '32x32' href = 'assets/logo/favicon-32x32.png' />
< link rel = 'icon' type = 'image/png' sizes = '16x16' href = 'assets/logo/favicon-16x16.png' />
< link rel = 'manifest' href = 'site.webmanifest' />
< meta name = 'msapplication-TileColor' content = '#fee9cd' />
< meta name = 'theme-color' content = '#ffffff' />
< /Helmet>
< GlobalStyle background = { bodyStyle . background } color = { bodyStyle . color } fontFamily = { bodyStyle . fontFamily } />
< Routes >
< Route exact path = '/' element = { < Home setBodyStyle = { setBodyStyle } /> } />
< Route path = { `/p/:subplebbitAddress` } element = { < Board setBodyStyle = { setBodyStyle } /> } >
< Route path = 'post' element = { < Board /> } />
< Route path = 'settings' element = { < Board /> } />
< /Route>
< Route path = { `/p/:subplebbitAddress/c/:threadCid` } element = { < Thread setBodyStyle = { setBodyStyle } /> } >
< Route path = 'post' element = { < Thread /> } />
< Route path = 'settings' element = { < Thread /> } />
< /Route>
< Route path = { `/p/:subplebbitAddress/catalog` } element = { < Catalog setBodyStyle = { setBodyStyle } /> } >
< Route path = 'post' element = { < Catalog /> } />
< Route path = 'settings' element = { < Catalog /> } />
< /Route>
< Route path = { `/p/:subplebbitAddress/description` } element = { < Description setBodyStyle = { setBodyStyle } /> } >
< Route path = 'settings' element = { < Description /> } />
< /Route>
< Route path = { `/p/:subplebbitAddress/rules` } element = { < Rules setBodyStyle = { setBodyStyle } /> } >
< Route path = 'settings' element = { < Rules /> } />
< /Route>
< Route path = { `/profile/c/:index` } element = { < Pending setBodyStyle = { setBodyStyle } /> } >
< Route path = 'settings' element = { < Pending /> } />
< /Route>
< Route path = { `/p/subscriptions` } element = { < Subscriptions setBodyStyle = { setBodyStyle } /> } >
< Route path = 'settings' element = { < Subscriptions /> } />
< /Route>
< Route path = { `p/subscriptions/catalog` } element = { < SubscriptionsCatalog setBodyStyle = { setBodyStyle } /> } >
< Route path = 'settings' element = { < SubscriptionsCatalog /> } />
< /Route>
< Route path = { `p/all` } element = { < All setBodyStyle = { setBodyStyle } /> } >
< Route path = 'settings' element = { < All /> } />
< /Route>
< Route path = { `p/all/catalog` } element = { < AllCatalog setBodyStyle = { setBodyStyle } /> } >
< Route path = 'settings' element = { < AllCatalog /> } />
< /Route>
< Route path = '*' element = { < NotFound setBodyStyle = { setBodyStyle } /> } />
< /Routes>
< Toast />
< CaptchaModal selectedStyle = { selectedStyle } isOpen = { isCaptchaOpen } closeModal = {() => setIsCaptchaOpen ( false )} />
< /div>
);
}