feat: community mode (#738)

Co-authored-by: CXPLAY <62034099+cxplay@users.noreply.github.com>
This commit is contained in:
Cody Tseng 2026-01-24 00:09:10 +08:00 committed by GitHub
parent 686b1f9998
commit ed8a22d5bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 303 additions and 101 deletions

View file

@ -0,0 +1,32 @@
import FollowingFeed from '@/components/FollowingFeed'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { TPageRef } from '@/types'
import { UsersRound } from 'lucide-react'
import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next'
const FollowingPage = forwardRef<TPageRef>((_, ref) => {
return (
<PrimaryPageLayout
pageName="following"
titlebar={<FollowingPageTitlebar />}
displayScrollToTopButton
ref={ref}
>
<FollowingFeed />
</PrimaryPageLayout>
)
})
FollowingPage.displayName = 'FollowingPage'
export default FollowingPage
function FollowingPageTitlebar() {
const { t } = useTranslation()
return (
<div className="flex h-full items-center gap-2 pl-3">
<UsersRound />
<div className="text-lg font-semibold">{t('Following')}</div>
</div>
)
}

View file

@ -2,6 +2,7 @@ import FeedSwitcher from '@/components/FeedSwitcher'
import RelayIcon from '@/components/RelayIcon'
import { Drawer, DrawerContent } from '@/components/ui/drawer'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { IS_COMMUNITY_MODE, COMMUNITY_RELAY_SETS, COMMUNITY_RELAYS } from '@/constants'
import { simplifyUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
@ -15,6 +16,10 @@ export default function FeedButton({ className }: { className?: string }) {
const { isSmallScreen } = useScreenSize()
const [open, setOpen] = useState(false)
if (IS_COMMUNITY_MODE && COMMUNITY_RELAY_SETS.length + COMMUNITY_RELAYS.length <= 1) {
return <FeedSwitcherTrigger className={className} />
}
if (isSmallScreen) {
return (
<>
@ -61,7 +66,8 @@ const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
const { relaySets } = useFavoriteRelays()
const activeRelaySet = useMemo(() => {
return feedInfo?.feedType === 'relays' && feedInfo.id
? relaySets.find((set) => set.id === feedInfo.id)
? (relaySets.find((set) => set.id === feedInfo.id) ??
COMMUNITY_RELAY_SETS.find((set) => set.id === feedInfo.id))
: undefined
}, [feedInfo, relaySets])
const title = useMemo(() => {
@ -78,7 +84,7 @@ const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
return simplifyUrl(feedInfo?.id ?? '')
}
if (feedInfo?.feedType === 'relays') {
return activeRelaySet?.name ?? activeRelaySet?.id
return feedInfo.name ?? activeRelaySet?.name ?? activeRelaySet?.id
}
}, [feedInfo, activeRelaySet])
@ -92,15 +98,22 @@ const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
return <Server />
}, [feedInfo])
const clickable =
!IS_COMMUNITY_MODE || COMMUNITY_RELAY_SETS.length + COMMUNITY_RELAYS.length > 1
return (
<div
className={cn('clickable flex h-full items-center gap-2 rounded-xl px-3', className)}
className={cn(
'flex h-full items-center gap-2 rounded-xl px-3',
clickable && 'clickable',
className
)}
ref={ref}
{...props}
>
{icon}
<div className="truncate text-lg font-semibold">{title}</div>
<ChevronDown />
{clickable && <ChevronDown />}
</div>
)
}

View file

@ -1,80 +0,0 @@
import NormalFeed from '@/components/NormalFeed'
import { Button } from '@/components/ui/button'
import { usePrimaryPage } from '@/PageManager'
import { useFollowList } from '@/providers/FollowListProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types'
import { Compass, Search, UserPlus } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function FollowingFeed() {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { followingSet } = useFollowList()
const { navigate } = usePrimaryPage()
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
const [hasFollowings, setHasFollowings] = useState<boolean | null>(null)
const [refreshCount, setRefreshCount] = useState(0)
const initializedRef = useRef(false)
useEffect(() => {
if (initializedRef.current) return
async function init() {
if (!pubkey) {
setSubRequests([])
setHasFollowings(null)
return
}
const followings = await client.fetchFollowings(pubkey)
setHasFollowings(followings.length > 0)
setSubRequests(await client.generateSubRequestsForPubkeys([pubkey, ...followings], pubkey))
if (followings.length) {
initializedRef.current = true
}
}
init()
}, [pubkey, followingSet, refreshCount])
// Show empty state when user has no followings
if (hasFollowings === false && subRequests.length > 0) {
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center px-6 text-center">
<UserPlus size={64} className="mb-4 text-muted-foreground" strokeWidth={1.5} />
<h2 className="mb-2 text-2xl font-semibold">{t('Welcome to Jumble!')}</h2>
<p className="mb-6 max-w-md text-muted-foreground">
{t(
'Your feed is empty because you are not following anyone yet. Start by exploring interesting content and following users you like!'
)}
</p>
<div className="flex w-full max-w-md flex-col gap-3 sm:flex-row">
<Button size="lg" onClick={() => navigate('explore')} className="w-full">
<Compass className="size-5" />
{t('Explore')}
</Button>
<Button size="lg" variant="outline" onClick={() => navigate('search')} className="w-full">
<Search className="size-5" />
{t('Search Users')}
</Button>
</div>
</div>
)
}
return (
<NormalFeed
subRequests={subRequests}
onRefresh={() => {
initializedRef.current = false
setRefreshCount((count) => count + 1)
}}
isMainFeed
isPubkeyFeed
/>
)
}

View file

@ -1,4 +1,5 @@
import { usePrimaryPage, useSecondaryPage } from '@/PageManager'
import FollowingFeed from '@/components/FollowingFeed'
import PostEditor from '@/components/PostEditor'
import RelayInfo from '@/components/RelayInfo'
import { Button } from '@/components/ui/button'
@ -21,7 +22,6 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import FeedButton from './FeedButton'
import FollowingFeed from './FollowingFeed'
import PinnedFeed from './PinnedFeed'
import RelaysFeed from './RelaysFeed'

View file

@ -1,6 +1,7 @@
import MailboxSetting from '@/components/MailboxSetting'
import FavoriteRelaysSetting from '@/components/FavoriteRelaysSetting'
import MailboxSetting from '@/components/MailboxSetting'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { IS_COMMUNITY_MODE } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -20,6 +21,16 @@ const RelaySettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
}
}, [])
if (IS_COMMUNITY_MODE) {
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Relay settings')}>
<div className="space-y-4 px-4 py-3">
<MailboxSetting />
</div>
</SecondaryPageLayout>
)
}
return (
<SecondaryPageLayout ref={ref} index={index} title={t('Relay settings')}>
<Tabs value={tabValue} onValueChange={setTabValue} className="space-y-4 px-4 py-3">