index.tsx raw

   1  import { usePrimaryPage } from '@/PageManager'
   2  import RelayInfo from '@/components/RelayInfo'
   3  import SmeshLoader from '@/components/SmeshLoader'
   4  import { Button } from '@/components/ui/button'
   5  import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
   6  import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
   7  import { useFeed } from '@/providers/FeedProvider'
   8  import { useNostr } from '@/providers/NostrProvider'
   9  import { TPageRef } from '@/types'
  10  import { Compass, Info, LogIn } from 'lucide-react'
  11  import {
  12    Dispatch,
  13    forwardRef,
  14    SetStateAction,
  15    useEffect,
  16    useImperativeHandle,
  17    useRef,
  18    useState
  19  } from 'react'
  20  import { useTranslation } from 'react-i18next'
  21  import FeedButton from './FeedButton'
  22  import FollowingFeed from './FollowingFeed'
  23  import PinnedFeed from './PinnedFeed'
  24  import RelaysFeed from './RelaysFeed'
  25  
  26  const NoteListPage = forwardRef<TPageRef>((_, ref) => {
  27    const { addRelayUrls, removeRelayUrls } = useCurrentRelays()
  28    const layoutRef = useRef<TPageRef>(null)
  29    const { pubkey } = useNostr()
  30    const { feedInfo, relayUrls, isReady, switchFeed } = useFeed()
  31    const [showRelayDetails, setShowRelayDetails] = useState(false)
  32  
  33    useImperativeHandle(ref, () => layoutRef.current as TPageRef)
  34  
  35    useEffect(() => {
  36      if (layoutRef.current) {
  37        layoutRef.current.scrollToTop('instant')
  38      }
  39    }, [JSON.stringify(relayUrls), feedInfo])
  40  
  41    useEffect(() => {
  42      if (relayUrls.length) {
  43        addRelayUrls(relayUrls)
  44        return () => {
  45          removeRelayUrls(relayUrls)
  46        }
  47      }
  48    }, [relayUrls])
  49  
  50    let content: React.ReactNode = null
  51    if (!isReady) {
  52      content = (
  53        <div className="flex items-center justify-center pt-16">
  54          <SmeshLoader className="w-48 h-48" />
  55        </div>
  56      )
  57    } else if (!feedInfo) {
  58      content = <WelcomeGuide />
  59    } else if (feedInfo.feedType === 'following' && !pubkey) {
  60      switchFeed(null)
  61      return null
  62    } else if (feedInfo.feedType === 'pinned' && !pubkey) {
  63      switchFeed(null)
  64      return null
  65    } else if (feedInfo.feedType === 'following') {
  66      content = <FollowingFeed />
  67    } else if (feedInfo.feedType === 'pinned') {
  68      content = <PinnedFeed />
  69    } else {
  70      content = (
  71        <>
  72          {showRelayDetails && feedInfo.feedType === 'relay' && !!feedInfo.id && (
  73            <RelayInfo url={feedInfo.id!} className="mb-2 pt-3" />
  74          )}
  75          <RelaysFeed />
  76        </>
  77      )
  78    }
  79  
  80    return (
  81      <PrimaryPageLayout
  82        pageName="home"
  83        ref={layoutRef}
  84        titlebar={
  85          <NoteListPageTitlebar
  86            layoutRef={layoutRef}
  87            showRelayDetails={showRelayDetails}
  88            setShowRelayDetails={
  89              feedInfo?.feedType === 'relay' && !!feedInfo.id ? setShowRelayDetails : undefined
  90            }
  91          />
  92        }
  93        displayScrollToTopButton
  94      >
  95        {content}
  96      </PrimaryPageLayout>
  97    )
  98  })
  99  NoteListPage.displayName = 'NoteListPage'
 100  export default NoteListPage
 101  
 102  function NoteListPageTitlebar({
 103    layoutRef,
 104    showRelayDetails,
 105    setShowRelayDetails
 106  }: {
 107    layoutRef?: React.RefObject<TPageRef>
 108    showRelayDetails?: boolean
 109    setShowRelayDetails?: Dispatch<SetStateAction<boolean>>
 110  }) {
 111    return (
 112      <div className="flex gap-1 items-center h-full justify-between">
 113        <FeedButton className="flex-1 max-w-fit w-0" />
 114        <div className="shrink-0 flex gap-1 items-center">
 115          {setShowRelayDetails && (
 116            <Button
 117              variant="ghost"
 118              size="titlebar-icon"
 119              onClick={(e) => {
 120                e.stopPropagation()
 121                setShowRelayDetails((show) => !show)
 122  
 123                if (!showRelayDetails) {
 124                  layoutRef?.current?.scrollToTop('smooth')
 125                }
 126              }}
 127              className={showRelayDetails ? 'bg-accent/50' : ''}
 128            >
 129              <Info />
 130            </Button>
 131          )}
 132        </div>
 133      </div>
 134    )
 135  }
 136  
 137  function WelcomeGuide() {
 138    const { t } = useTranslation()
 139    const { navigate } = usePrimaryPage()
 140    const { checkLogin } = useNostr()
 141  
 142    return (
 143      <div className="flex flex-col items-center justify-center min-h-[60vh] px-4 text-center space-y-6">
 144        <SmeshLoader className="w-40 h-40" />
 145        <div className="space-y-2">
 146          <h2 className="text-2xl font-bold">{t('Welcome to Smesh')}</h2>
 147          <p className="text-muted-foreground max-w-md">
 148            {t(
 149              'Smesh is a client focused on browsing relays. Get started by exploring interesting relays or login to view your following feed.'
 150            )}
 151          </p>
 152        </div>
 153  
 154        <div className="flex flex-col sm:flex-row gap-3 w-full max-w-md">
 155          <Button size="lg" className="w-full" onClick={() => navigate('explore')}>
 156            <Compass className="size-5" />
 157            {t('Explore')}
 158          </Button>
 159  
 160          <Button size="lg" className="w-full" variant="outline" onClick={() => checkLogin()}>
 161            <LogIn className="size-5" />
 162            {t('Login')}
 163          </Button>
 164        </div>
 165      </div>
 166    )
 167  }
 168