feat: small screen (#8)
This commit is contained in:
parent
11d035f719
commit
8e0b91888f
27 changed files with 372 additions and 127 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Jumble</title>
|
<title>Jumble</title>
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
<!-- <meta
|
<!-- <meta
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,26 @@ import { FollowListProvider } from './providers/FollowListProvider'
|
||||||
import { NostrProvider } from './providers/NostrProvider'
|
import { NostrProvider } from './providers/NostrProvider'
|
||||||
import { NoteStatsProvider } from './providers/NoteStatsProvider'
|
import { NoteStatsProvider } from './providers/NoteStatsProvider'
|
||||||
import { RelaySettingsProvider } from './providers/RelaySettingsProvider'
|
import { RelaySettingsProvider } from './providers/RelaySettingsProvider'
|
||||||
|
import { ScreenSizeProvider } from './providers/ScreenSizeProvider'
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
export default function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<RelaySettingsProvider>
|
<ScreenSizeProvider>
|
||||||
<NostrProvider>
|
<RelaySettingsProvider>
|
||||||
<FollowListProvider>
|
<NostrProvider>
|
||||||
<NoteStatsProvider>
|
<FollowListProvider>
|
||||||
<PageManager>
|
<NoteStatsProvider>
|
||||||
<NoteListPage />
|
<PageManager>
|
||||||
</PageManager>
|
<NoteListPage />
|
||||||
<Toaster />
|
</PageManager>
|
||||||
</NoteStatsProvider>
|
<Toaster />
|
||||||
</FollowListProvider>
|
</NoteStatsProvider>
|
||||||
</NostrProvider>
|
</FollowListProvider>
|
||||||
</RelaySettingsProvider>
|
</NostrProvider>
|
||||||
|
</RelaySettingsProvider>
|
||||||
|
</ScreenSizeProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import HomePage from '@renderer/pages/secondary/HomePage'
|
||||||
import NotFoundPage from '@renderer/pages/secondary/NotFoundPage'
|
import NotFoundPage from '@renderer/pages/secondary/NotFoundPage'
|
||||||
import { cloneElement, createContext, useContext, useEffect, useState } from 'react'
|
import { cloneElement, createContext, useContext, useEffect, useState } from 'react'
|
||||||
import { routes } from './routes'
|
import { routes } from './routes'
|
||||||
|
import { useScreenSize } from './providers/ScreenSizeProvider'
|
||||||
|
|
||||||
type TPrimaryPageContext = {
|
type TPrimaryPageContext = {
|
||||||
refresh: () => void
|
refresh: () => void
|
||||||
|
|
@ -54,6 +55,7 @@ export function PageManager({
|
||||||
}) {
|
}) {
|
||||||
const [primaryPageKey, setPrimaryPageKey] = useState<number>(0)
|
const [primaryPageKey, setPrimaryPageKey] = useState<number>(0)
|
||||||
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([])
|
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([])
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.location.pathname !== '/') {
|
if (window.location.pathname !== '/') {
|
||||||
|
|
@ -83,6 +85,7 @@ export function PageManager({
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('popstate', onPopState)
|
window.addEventListener('popstate', onPopState)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('popstate', onPopState)
|
window.removeEventListener('popstate', onPopState)
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +109,34 @@ export function PageManager({
|
||||||
window.history.back()
|
window.history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<PrimaryPageContext.Provider value={{ refresh: refreshPrimary }}>
|
||||||
|
<SecondaryPageContext.Provider value={{ push: pushSecondary, pop: popSecondary }}>
|
||||||
|
<div className="h-full">
|
||||||
|
{!!secondaryStack.length &&
|
||||||
|
secondaryStack.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item.index}
|
||||||
|
className="absolute top-0 left-0 w-full h-full bg-background"
|
||||||
|
style={{ zIndex: index + 1 }}
|
||||||
|
>
|
||||||
|
{item.component}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
key={primaryPageKey}
|
||||||
|
className="absolute top-0 left-0 w-full h-full bg-background"
|
||||||
|
style={{ zIndex: 0 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SecondaryPageContext.Provider>
|
||||||
|
</PrimaryPageContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PrimaryPageContext.Provider value={{ refresh: refreshPrimary }}>
|
<PrimaryPageContext.Provider value={{ refresh: refreshPrimary }}>
|
||||||
<SecondaryPageContext.Provider value={{ push: pushSecondary, pop: popSecondary }}>
|
<SecondaryPageContext.Provider value={{ push: pushSecondary, pop: popSecondary }}>
|
||||||
|
|
@ -124,7 +155,7 @@ export function PageManager({
|
||||||
<div
|
<div
|
||||||
key={item.index}
|
key={item.index}
|
||||||
className="absolute top-0 left-0 w-full h-full bg-background"
|
className="absolute top-0 left-0 w-full h-full bg-background"
|
||||||
style={{ zIndex: index }}
|
style={{ zIndex: index + 1 }}
|
||||||
>
|
>
|
||||||
{item.component}
|
{item.component}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useFetchEvent } from '@renderer/hooks'
|
import { useFetchEvent } from '@renderer/hooks'
|
||||||
import { toNoStrudelArticle, toNoStrudelNote } from '@renderer/lib/link'
|
import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@renderer/lib/link'
|
||||||
import { kinds } from 'nostr-tools'
|
import { kinds } from 'nostr-tools'
|
||||||
import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard'
|
import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard'
|
||||||
|
|
||||||
|
|
@ -7,11 +7,15 @@ export function EmbeddedNote({ noteId }: { noteId: string }) {
|
||||||
const { event } = useFetchEvent(noteId)
|
const { event } = useFetchEvent(noteId)
|
||||||
|
|
||||||
return event && event.kind === kinds.ShortTextNote ? (
|
return event && event.kind === kinds.ShortTextNote ? (
|
||||||
<ShortTextNoteCard size="small" className="mt-2 w-full" event={event} hideStats />
|
<ShortTextNoteCard className="mt-2 w-full" event={event} embedded />
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
event?.kind === kinds.LongFormArticle ? toNoStrudelArticle(noteId) : toNoStrudelNote(noteId)
|
event?.kind === kinds.LongFormArticle
|
||||||
|
? toNoStrudelArticle(noteId)
|
||||||
|
: event?.kind === kinds.LiveEvent
|
||||||
|
? toNoStrudelStream(noteId)
|
||||||
|
: toNoStrudelNote(noteId)
|
||||||
}
|
}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-highlight hover:underline"
|
className="text-highlight hover:underline"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
import { useSecondaryPage } from '@renderer/PageManager'
|
||||||
|
import { toNoteList } from '@renderer/lib/link'
|
||||||
import { TEmbeddedRenderer } from './types'
|
import { TEmbeddedRenderer } from './types'
|
||||||
|
|
||||||
export function EmbeddedWebsocketUrl({ url }: { url: string }) {
|
export function EmbeddedWebsocketUrl({ url }: { url: string }) {
|
||||||
const { setTemporaryRelayUrls } = useRelaySettings()
|
const { push } = useSecondaryPage()
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="cursor-pointer px-1 rounded-md text-highlight border border-highlight/60 hover:border-highlight hover:bg-muted/60"
|
className="cursor-pointer px-1 rounded-md text-highlight border border-highlight/60 hover:border-highlight hover:bg-muted/60"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setTemporaryRelayUrls([url])
|
push(toNoteList({ relay: url }))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{url}
|
{url}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import client from '@renderer/services/client.service'
|
import client from '@renderer/services/client.service'
|
||||||
import { Repeat2 } from 'lucide-react'
|
|
||||||
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
import { Event, kinds, verifyEvent } from 'nostr-tools'
|
||||||
import Username from '../Username'
|
|
||||||
import ShortTextNoteCard from './ShortTextNoteCard'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import ShortTextNoteCard from './ShortTextNoteCard'
|
||||||
|
|
||||||
export default function RepostNoteCard({ event, className }: { event: Event; className?: string }) {
|
export default function RepostNoteCard({ event, className }: { event: Event; className?: string }) {
|
||||||
const { t } = useTranslation()
|
|
||||||
const targetEvent = useMemo(() => {
|
const targetEvent = useMemo(() => {
|
||||||
const targetEvent = event.content ? (JSON.parse(event.content) as Event) : null
|
const targetEvent = event.content ? (JSON.parse(event.content) as Event) : null
|
||||||
try {
|
try {
|
||||||
|
|
@ -23,18 +19,5 @@ export default function RepostNoteCard({ event, className }: { event: Event; cla
|
||||||
}, [event])
|
}, [event])
|
||||||
if (!targetEvent) return null
|
if (!targetEvent) return null
|
||||||
|
|
||||||
return (
|
return <ShortTextNoteCard className={className} reposter={event.pubkey} event={targetEvent} />
|
||||||
<div className={className}>
|
|
||||||
<div className="flex gap-1 mb-1 pl-4 text-sm items-center text-muted-foreground">
|
|
||||||
<Repeat2 size={16} className="shrink-0" />
|
|
||||||
<Username
|
|
||||||
userId={event.pubkey}
|
|
||||||
className="font-semibold truncate"
|
|
||||||
skeletonClassName="h-3"
|
|
||||||
/>
|
|
||||||
<div>{t('reposted')}</div>
|
|
||||||
</div>
|
|
||||||
<ShortTextNoteCard event={targetEvent} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,24 @@
|
||||||
import { Card } from '@renderer/components/ui/card'
|
|
||||||
import { useFetchEvent } from '@renderer/hooks'
|
import { useFetchEvent } from '@renderer/hooks'
|
||||||
import { getParentEventId, getRootEventId } from '@renderer/lib/event'
|
import { getParentEventId, getRootEventId } from '@renderer/lib/event'
|
||||||
import { toNote } from '@renderer/lib/link'
|
import { toNote } from '@renderer/lib/link'
|
||||||
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { useSecondaryPage } from '@renderer/PageManager'
|
import { useSecondaryPage } from '@renderer/PageManager'
|
||||||
|
import { Repeat2 } from 'lucide-react'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Note from '../Note'
|
import Note from '../Note'
|
||||||
|
import Username from '../Username'
|
||||||
|
|
||||||
export default function ShortTextNoteCard({
|
export default function ShortTextNoteCard({
|
||||||
event,
|
event,
|
||||||
className,
|
className,
|
||||||
size = 'normal',
|
reposter,
|
||||||
hideStats = false
|
embedded
|
||||||
}: {
|
}: {
|
||||||
event: Event
|
event: Event
|
||||||
className?: string
|
className?: string
|
||||||
size?: 'normal' | 'small'
|
reposter?: string
|
||||||
hideStats?: boolean
|
embedded?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const { event: rootEvent } = useFetchEvent(getRootEventId(event))
|
const { event: rootEvent } = useFetchEvent(getRootEventId(event))
|
||||||
|
|
@ -29,16 +32,37 @@ export default function ShortTextNoteCard({
|
||||||
push(toNote(event.id))
|
push(toNote(event.id))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
<RepostDescription reposter={reposter} className="max-sm:hidden pl-4" />
|
||||||
className={`hover:bg-muted/50 text-left cursor-pointer ${size === 'normal' ? 'p-4' : 'p-3'}`}
|
<div
|
||||||
|
className={`hover:bg-muted/50 text-left cursor-pointer ${embedded ? 'p-3 border rounded-lg' : 'p-4 sm:border sm:rounded-lg max-sm:border-b'}`}
|
||||||
>
|
>
|
||||||
|
<RepostDescription reposter={reposter} className="sm:hidden" />
|
||||||
<Note
|
<Note
|
||||||
size={size}
|
size={embedded ? 'small' : 'normal'}
|
||||||
event={event}
|
event={event}
|
||||||
parentEvent={parentEvent ?? rootEvent}
|
parentEvent={parentEvent ?? rootEvent}
|
||||||
hideStats={hideStats}
|
hideStats={embedded}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RepostDescription({
|
||||||
|
reposter,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
reposter?: string | null
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
if (!reposter) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex gap-1 text-sm items-center text-muted-foreground mb-1', className)}>
|
||||||
|
<Repeat2 size={16} className="shrink-0" />
|
||||||
|
<Username userId={reposter} className="font-semibold truncate" skeletonClassName="h-3" />
|
||||||
|
<div>{t('reposted')}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ export default function NoteList({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={cn('flex flex-col gap-4', className)}>
|
<div className={cn('flex flex-col sm:gap-4', className)}>
|
||||||
{events.map((event, i) => (
|
{events.map((event, i) => (
|
||||||
<NoteCard key={`${i}-${event.id}`} className="w-full" event={event} />
|
<NoteCard key={`${i}-${event.id}`} className="w-full" event={event} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import RelayGroup from './RelayGroup'
|
||||||
import TemporaryRelayGroup from './TemporaryRelayGroup'
|
import TemporaryRelayGroup from './TemporaryRelayGroup'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function RelaySettings() {
|
export default function RelaySettings({ hideTitle = false }: { hideTitle?: boolean }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { relayGroups, addRelayGroup } = useRelaySettings()
|
const { relayGroups, addRelayGroup } = useRelaySettings()
|
||||||
const [newGroupName, setNewGroupName] = useState('')
|
const [newGroupName, setNewGroupName] = useState('')
|
||||||
|
|
@ -47,7 +47,7 @@ export default function RelaySettings() {
|
||||||
return (
|
return (
|
||||||
<RelaySettingsComponentProvider>
|
<RelaySettingsComponentProvider>
|
||||||
<div ref={dummyRef} tabIndex={-1} style={{ position: 'absolute', opacity: 0 }}></div>
|
<div ref={dummyRef} tabIndex={-1} style={{ position: 'absolute', opacity: 0 }}></div>
|
||||||
<div className="text-lg font-semibold mb-4">{t('Relay Settings')}</div>
|
{!hideTitle && <div className="text-lg font-semibold mb-4">{t('Relay Settings')}</div>}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<TemporaryRelayGroup />
|
<TemporaryRelayGroup />
|
||||||
{relayGroups.map((group, index) => (
|
{relayGroups.map((group, index) => (
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,31 @@ import RelaySettings from '@renderer/components/RelaySettings'
|
||||||
import { Button } from '@renderer/components/ui/button'
|
import { Button } from '@renderer/components/ui/button'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'
|
||||||
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||||
|
import { toRelaySettings } from '@renderer/lib/link'
|
||||||
|
import { SecondaryPageLink } from '@renderer/PageManager'
|
||||||
|
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||||
import { Server } from 'lucide-react'
|
import { Server } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function RelaySettingsPopover({
|
export default function RelaySettingsButton({
|
||||||
variant = 'titlebar'
|
variant = 'titlebar'
|
||||||
}: {
|
}: {
|
||||||
variant?: 'titlebar' | 'sidebar'
|
variant?: 'titlebar' | 'sidebar'
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<SecondaryPageLink to={toRelaySettings()}>
|
||||||
|
<Button variant={variant} size={variant} title={t('Relay settings')}>
|
||||||
|
<Server />
|
||||||
|
{variant === 'sidebar' && <div>{t('SidebarRelays')}</div>}
|
||||||
|
</Button>
|
||||||
|
</SecondaryPageLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import { Button } from '@renderer/components/ui/button'
|
import { Button } from '@renderer/components/ui/button'
|
||||||
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { ChevronUp } from 'lucide-react'
|
import { ChevronUp } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export default function ScrollToTopButton({
|
export default function ScrollToTopButton({
|
||||||
scrollAreaRef
|
scrollAreaRef,
|
||||||
|
className
|
||||||
}: {
|
}: {
|
||||||
scrollAreaRef: React.RefObject<HTMLDivElement>
|
scrollAreaRef: React.RefObject<HTMLDivElement>
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const [showScrollToTop, setShowScrollToTop] = useState(false)
|
const [showScrollToTop, setShowScrollToTop] = useState(false)
|
||||||
|
|
||||||
|
|
@ -15,7 +18,7 @@ export default function ScrollToTopButton({
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (scrollAreaRef.current) {
|
if (scrollAreaRef.current) {
|
||||||
setShowScrollToTop(scrollAreaRef.current.scrollTop > 1000)
|
setShowScrollToTop(scrollAreaRef.current.scrollTop > 600)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,7 +33,10 @@ export default function ScrollToTopButton({
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary-2"
|
variant="secondary-2"
|
||||||
className={`absolute bottom-8 right-2 rounded-full w-10 h-10 p-0 hover:text-background transition-transform ${showScrollToTop ? '' : 'translate-y-20'}`}
|
className={cn(
|
||||||
|
`absolute bottom-8 right-2 rounded-full w-10 h-10 p-0 hover:text-background transition-transform ${showScrollToTop ? '' : 'translate-y-20'}`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
onClick={handleScrollToTop}
|
onClick={handleScrollToTop}
|
||||||
>
|
>
|
||||||
<ChevronUp />
|
<ChevronUp />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
CommandList
|
CommandList
|
||||||
} from '@renderer/components/ui/command'
|
} from '@renderer/components/ui/command'
|
||||||
import { useSearchProfiles } from '@renderer/hooks'
|
import { useSearchProfiles } from '@renderer/hooks'
|
||||||
|
import { isMacOS } from '@renderer/lib/env'
|
||||||
import { toNote, toNoteList, toProfile, toProfileList } from '@renderer/lib/link'
|
import { toNote, toNoteList, toProfile, toProfileList } from '@renderer/lib/link'
|
||||||
import { generateImageByPubkey } from '@renderer/lib/pubkey'
|
import { generateImageByPubkey } from '@renderer/lib/pubkey'
|
||||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
||||||
|
|
@ -80,7 +81,11 @@ export function SearchDialog({ open, setOpen }: { open: boolean; setOpen: Dispat
|
||||||
}, [input])
|
}, [input])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<CommandDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
classNames={{ content: isMacOS() ? 'max-sm:top-9' : 'max-sm:top-0' }}
|
||||||
|
>
|
||||||
<CommandInput value={input} onValueChange={setInput} />
|
<CommandInput value={input} onValueChange={setInput} />
|
||||||
<CommandList>{list}</CommandList>
|
<CommandList>{list}</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import AboutInfoDialog from '../AboutInfoDialog'
|
||||||
import AccountButton from '../AccountButton'
|
import AccountButton from '../AccountButton'
|
||||||
import PostButton from '../PostButton'
|
import PostButton from '../PostButton'
|
||||||
import RefreshButton from '../RefreshButton'
|
import RefreshButton from '../RefreshButton'
|
||||||
import RelaySettingsPopover from '../RelaySettingsPopover'
|
import RelaySettingsButton from '../RelaySettingsButton'
|
||||||
import SearchButton from '../SearchButton'
|
import SearchButton from '../SearchButton'
|
||||||
|
|
||||||
export default function PrimaryPageSidebar() {
|
export default function PrimaryPageSidebar() {
|
||||||
|
|
@ -20,7 +20,7 @@ export default function PrimaryPageSidebar() {
|
||||||
<SecondaryPageLink to={toHome()}>Jumble</SecondaryPageLink>
|
<SecondaryPageLink to={toHome()}>Jumble</SecondaryPageLink>
|
||||||
</div>
|
</div>
|
||||||
<PostButton variant="sidebar" />
|
<PostButton variant="sidebar" />
|
||||||
<RelaySettingsPopover variant="sidebar" />
|
<RelaySettingsButton variant="sidebar" />
|
||||||
<SearchButton variant="sidebar" />
|
<SearchButton variant="sidebar" />
|
||||||
<RefreshButton variant="sidebar" />
|
<RefreshButton variant="sidebar" />
|
||||||
{!IS_ELECTRON && (
|
{!IS_ELECTRON && (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { isMacOS } from '@renderer/lib/env'
|
||||||
import { cn } from '@renderer/lib/utils'
|
import { cn } from '@renderer/lib/utils'
|
||||||
|
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||||
|
|
||||||
export function Titlebar({
|
export function Titlebar({
|
||||||
children,
|
children,
|
||||||
|
|
@ -7,10 +9,21 @@ export function Titlebar({
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
|
if (isMacOS() && isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<div className="absolute top-0 w-full z-50 bg-background/80 backdrop-blur-md font-semibold">
|
||||||
|
<div className="draggable h-9 w-full" />
|
||||||
|
<div className={cn('h-11 flex gap-1 items-center', className)}>{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'draggable absolute top-0 w-full h-9 z-50 bg-background/80 backdrop-blur-md flex items-center font-semibold space-x-1 px-2',
|
'draggable absolute top-0 w-full h-9 z-50 bg-background/80 backdrop-blur-md flex items-center font-semibold gap-1 px-2',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,23 @@ const Command = React.forwardRef<
|
||||||
))
|
))
|
||||||
Command.displayName = CommandPrimitive.displayName
|
Command.displayName = CommandPrimitive.displayName
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
const CommandDialog = ({
|
||||||
|
children,
|
||||||
|
classNames,
|
||||||
|
...props
|
||||||
|
}: DialogProps & { classNames?: { content?: string } }) => {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogHeader className="hidden">
|
<DialogHeader className="hidden">
|
||||||
<DialogTitle />
|
<DialogTitle />
|
||||||
<DialogDescription />
|
<DialogDescription />
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-lg top-4 translate-y-0 data-[state=closed]:slide-out-to-top-0 data-[state=open]:slide-in-from-top-0">
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
'overflow-hidden p-0 shadow-lg top-4 translate-y-0 data-[state=closed]:slide-out-to-top-0 data-[state=open]:slide-in-from-top-0',
|
||||||
|
classNames?.content
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Command
|
<Command
|
||||||
shouldFilter={false}
|
shouldFilter={false}
|
||||||
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-8 w-full rounded-lg p-2 border border-muted ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent disabled:cursor-not-allowed disabled:opacity-50',
|
'flex h-8 w-full rounded-lg p-2 bg-background border border-muted ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import AccountButton from '@renderer/components/AccountButton'
|
import AccountButton from '@renderer/components/AccountButton'
|
||||||
import PostButton from '@renderer/components/PostButton'
|
import PostButton from '@renderer/components/PostButton'
|
||||||
import RefreshButton from '@renderer/components/RefreshButton'
|
import RefreshButton from '@renderer/components/RefreshButton'
|
||||||
import RelaySettingsPopover from '@renderer/components/RelaySettingsPopover'
|
import RelaySettingsButton from '@renderer/components/RelaySettingsButton'
|
||||||
import ScrollToTopButton from '@renderer/components/ScrollToTopButton'
|
import ScrollToTopButton from '@renderer/components/ScrollToTopButton'
|
||||||
import SearchButton from '@renderer/components/SearchButton'
|
import SearchButton from '@renderer/components/SearchButton'
|
||||||
import { Titlebar } from '@renderer/components/Titlebar'
|
import { Titlebar } from '@renderer/components/Titlebar'
|
||||||
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||||
import { isMacOS } from '@renderer/lib/env'
|
import { isMacOS } from '@renderer/lib/env'
|
||||||
|
import { cn } from '@renderer/lib/utils'
|
||||||
|
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
||||||
|
|
||||||
const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode }, ref) => {
|
const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode }, ref) => {
|
||||||
|
|
@ -23,9 +25,15 @@ const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea ref={scrollAreaRef} className="h-full w-full" scrollBarClassName="pt-9 xl:pt-0">
|
<ScrollArea
|
||||||
|
ref={scrollAreaRef}
|
||||||
|
className="h-full w-full"
|
||||||
|
scrollBarClassName="pt-9 max-sm:pt-0 xl:pt-0"
|
||||||
|
>
|
||||||
<PrimaryPageTitlebar />
|
<PrimaryPageTitlebar />
|
||||||
<div className="px-4 pb-4 pt-11 xl:pt-4">{children}</div>
|
<div className={cn('sm:px-4 pb-4 pt-11 xl:pt-4', isMacOS() ? 'max-sm:pt-20' : 'max-sm:pt-9')}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
<ScrollToTopButton scrollAreaRef={scrollAreaRef} />
|
<ScrollToTopButton scrollAreaRef={scrollAreaRef} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
|
|
@ -37,7 +45,23 @@ export type TPrimaryPageLayoutRef = {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PrimaryPageTitlebar() {
|
function PrimaryPageTitlebar() {
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<Titlebar className="justify-between px-4">
|
||||||
|
<div className="text-2xl font-extrabold font-mono">Jumble</div>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<SearchButton />
|
||||||
|
<PostButton />
|
||||||
|
<RelaySettingsButton />
|
||||||
|
<AccountButton />
|
||||||
|
</div>
|
||||||
|
</Titlebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Titlebar className={`justify-between xl:hidden ${isMacOS() ? 'pl-20' : ''}`}>
|
<Titlebar className={`justify-between xl:hidden ${isMacOS() ? 'pl-20' : ''}`}>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
|
|
@ -47,7 +71,7 @@ export function PrimaryPageTitlebar() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
<RelaySettingsPopover />
|
<RelaySettingsButton />
|
||||||
</div>
|
</div>
|
||||||
</Titlebar>
|
</Titlebar>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import ScrollToTopButton from '@renderer/components/ScrollToTopButton'
|
||||||
import ThemeToggle from '@renderer/components/ThemeToggle'
|
import ThemeToggle from '@renderer/components/ThemeToggle'
|
||||||
import { Titlebar } from '@renderer/components/Titlebar'
|
import { Titlebar } from '@renderer/components/Titlebar'
|
||||||
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||||
|
import { isMacOS } from '@renderer/lib/env'
|
||||||
|
import { cn } from '@renderer/lib/utils'
|
||||||
|
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
|
||||||
export default function SecondaryPageLayout({
|
export default function SecondaryPageLayout({
|
||||||
|
|
@ -18,7 +21,9 @@ export default function SecondaryPageLayout({
|
||||||
return (
|
return (
|
||||||
<ScrollArea ref={scrollAreaRef} className="h-full" scrollBarClassName="pt-9">
|
<ScrollArea ref={scrollAreaRef} className="h-full" scrollBarClassName="pt-9">
|
||||||
<SecondaryPageTitlebar content={titlebarContent} hideBackButton={hideBackButton} />
|
<SecondaryPageTitlebar content={titlebarContent} hideBackButton={hideBackButton} />
|
||||||
<div className="px-4 pb-4 pt-11 w-full h-full">{children}</div>
|
<div className={cn('sm:px-4 pb-4 pt-11 w-full h-full', isMacOS() ? 'max-sm:pt-20' : '')}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
<ScrollToTopButton scrollAreaRef={scrollAreaRef} />
|
<ScrollToTopButton scrollAreaRef={scrollAreaRef} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
|
|
@ -31,6 +36,17 @@ export function SecondaryPageTitlebar({
|
||||||
content?: React.ReactNode
|
content?: React.ReactNode
|
||||||
hideBackButton?: boolean
|
hideBackButton?: boolean
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<Titlebar className="pl-2">
|
||||||
|
<BackButton hide={hideBackButton} />
|
||||||
|
<div className="truncate text-lg">{content}</div>
|
||||||
|
</Titlebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Titlebar className="justify-between">
|
<Titlebar className="justify-between">
|
||||||
<div className="flex items-center gap-1 flex-1 w-0">
|
<div className="flex items-center gap-1 flex-1 w-0">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
export const toHome = () => '/'
|
export const toHome = () => '/'
|
||||||
export const toNote = (eventId: string) => `/note/${eventId}`
|
export const toNote = (eventId: string) => `/note/${eventId}`
|
||||||
export const toNoteList = ({ hashtag, search }: { hashtag?: string; search?: string }) => {
|
export const toNoteList = ({
|
||||||
|
hashtag,
|
||||||
|
search,
|
||||||
|
relay
|
||||||
|
}: {
|
||||||
|
hashtag?: string
|
||||||
|
search?: string
|
||||||
|
relay?: string
|
||||||
|
}) => {
|
||||||
const path = '/note'
|
const path = '/note'
|
||||||
const query = new URLSearchParams()
|
const query = new URLSearchParams()
|
||||||
if (hashtag) query.set('t', hashtag.toLowerCase())
|
if (hashtag) query.set('t', hashtag.toLowerCase())
|
||||||
if (search) query.set('s', search)
|
if (search) query.set('s', search)
|
||||||
|
if (relay) query.set('relay', relay)
|
||||||
return `${path}?${query.toString()}`
|
return `${path}?${query.toString()}`
|
||||||
}
|
}
|
||||||
export const toProfile = (pubkey: string) => `/user/${pubkey}`
|
export const toProfile = (pubkey: string) => `/user/${pubkey}`
|
||||||
|
|
@ -15,7 +24,9 @@ export const toProfileList = ({ search }: { search?: string }) => {
|
||||||
return `${path}?${query.toString()}`
|
return `${path}?${query.toString()}`
|
||||||
}
|
}
|
||||||
export const toFollowingList = (pubkey: string) => `/user/${pubkey}/following`
|
export const toFollowingList = (pubkey: string) => `/user/${pubkey}/following`
|
||||||
|
export const toRelaySettings = () => '/relay-settings'
|
||||||
|
|
||||||
export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}`
|
export const toNoStrudelProfile = (id: string) => `https://nostrudel.ninja/#/u/${id}`
|
||||||
export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}`
|
export const toNoStrudelNote = (id: string) => `https://nostrudel.ninja/#/n/${id}`
|
||||||
export const toNoStrudelArticle = (id: string) => `https://nostrudel.ninja/#/articles/${id}`
|
export const toNoStrudelArticle = (id: string) => `https://nostrudel.ninja/#/articles/${id}`
|
||||||
|
export const toNoStrudelStream = (id: string) => `https://nostrudel.ninja/#/streams/${id}`
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default function FollowingListPage({ id }: { id?: string }) {
|
||||||
: t('following')
|
: t('following')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 max-sm:px-4">
|
||||||
{visibleFollowings.map((pubkey, index) => (
|
{visibleFollowings.map((pubkey, index) => (
|
||||||
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import NoteList from '@renderer/components/NoteList'
|
import NoteList from '@renderer/components/NoteList'
|
||||||
import { useSearchParams } from '@renderer/hooks'
|
import { useSearchParams } from '@renderer/hooks'
|
||||||
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout'
|
||||||
|
import { isWebsocketUrl } from '@renderer/lib/url'
|
||||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
||||||
import { Filter } from 'nostr-tools'
|
import { Filter } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
@ -10,19 +11,31 @@ export default function NoteListPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { relayUrls, searchableRelayUrls } = useRelaySettings()
|
const { relayUrls, searchableRelayUrls } = useRelaySettings()
|
||||||
const { searchParams } = useSearchParams()
|
const { searchParams } = useSearchParams()
|
||||||
const [title, filter] = useMemo<[string, Filter] | [undefined, undefined]>(() => {
|
const {
|
||||||
|
title = '',
|
||||||
|
filter,
|
||||||
|
specificRelayUrl
|
||||||
|
} = useMemo<{
|
||||||
|
title?: string
|
||||||
|
filter?: Filter
|
||||||
|
specificRelayUrl?: string
|
||||||
|
}>(() => {
|
||||||
const hashtag = searchParams.get('t')
|
const hashtag = searchParams.get('t')
|
||||||
if (hashtag) {
|
if (hashtag) {
|
||||||
return [`# ${hashtag}`, { '#t': [hashtag] }]
|
return { title: `# ${hashtag}`, filter: { '#t': [hashtag] } }
|
||||||
}
|
}
|
||||||
const search = searchParams.get('s')
|
const search = searchParams.get('s')
|
||||||
if (search) {
|
if (search) {
|
||||||
return [`${t('search')}: ${search}`, { search }]
|
return { title: `${t('search')}: ${search}`, filter: { search } }
|
||||||
}
|
}
|
||||||
return [undefined, undefined]
|
const relayUrl = searchParams.get('relay')
|
||||||
|
if (relayUrl && isWebsocketUrl(relayUrl)) {
|
||||||
|
return { title: relayUrl, specificRelayUrl: relayUrl }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
if (!filter || (filter.search && searchableRelayUrls.length === 0)) {
|
if (filter?.search && searchableRelayUrls.length === 0) {
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout titlebarContent={title}>
|
<SecondaryPageLayout titlebarContent={title}>
|
||||||
<div className="text-center text-sm text-muted-foreground">
|
<div className="text-center text-sm text-muted-foreground">
|
||||||
|
|
@ -34,7 +47,11 @@ export default function NoteListPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout titlebarContent={title}>
|
<SecondaryPageLayout titlebarContent={title}>
|
||||||
<NoteList key={title} filter={filter} relayUrls={relayUrls} />
|
<NoteList
|
||||||
|
key={title}
|
||||||
|
filter={filter}
|
||||||
|
relayUrls={specificRelayUrl ? [specificRelayUrl] : relayUrls}
|
||||||
|
/>
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,10 @@ export default function NotePage({ id }: { id?: string }) {
|
||||||
|
|
||||||
if (!event && isFetching) {
|
if (!event && isFetching) {
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout titlebarContent="note">
|
<SecondaryPageLayout titlebarContent={t('note')}>
|
||||||
<Skeleton className="w-10 h-10 rounded-full" />
|
<div className="max-sm:px-4">
|
||||||
|
<Skeleton className="w-10 h-10 rounded-full" />
|
||||||
|
</div>
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -31,11 +33,13 @@ export default function NotePage({ id }: { id?: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout titlebarContent={t('note')}>
|
<SecondaryPageLayout titlebarContent={t('note')}>
|
||||||
<ParentNote key={`root-note-${event.id}`} eventId={rootEventId} />
|
<div className="max-sm:px-4">
|
||||||
<ParentNote key={`parent-note-${event.id}`} eventId={parentEventId} />
|
<ParentNote key={`root-note-${event.id}`} eventId={rootEventId} />
|
||||||
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
<ParentNote key={`parent-note-${event.id}`} eventId={parentEventId} />
|
||||||
|
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
||||||
|
</div>
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} />
|
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="max-sm:px-2" />
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export default function ProfileListPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout titlebarContent={title}>
|
<SecondaryPageLayout titlebarContent={title}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 max-sm:px-4">
|
||||||
{Array.from(pubkeySet).map((pubkey, index) => (
|
{Array.from(pubkeySet).map((pubkey, index) => (
|
||||||
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
<UserItem key={`${index}-${pubkey}`} pubkey={pubkey} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,12 @@ import { generateImageByPubkey } from '@renderer/lib/pubkey'
|
||||||
import { SecondaryPageLink } from '@renderer/PageManager'
|
import { SecondaryPageLink } from '@renderer/PageManager'
|
||||||
import { useFollowList } from '@renderer/providers/FollowListProvider'
|
import { useFollowList } from '@renderer/providers/FollowListProvider'
|
||||||
import { useNostr } from '@renderer/providers/NostrProvider'
|
import { useNostr } from '@renderer/providers/NostrProvider'
|
||||||
|
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import NotFoundPage from '../NotFoundPage'
|
import NotFoundPage from '../NotFoundPage'
|
||||||
import PubkeyCopy from './PubkeyCopy'
|
import PubkeyCopy from './PubkeyCopy'
|
||||||
import QrCodePopover from './QrCodePopover'
|
import QrCodePopover from './QrCodePopover'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
|
|
||||||
|
|
||||||
export default function ProfilePage({ id }: { id?: string }) {
|
export default function ProfilePage({ id }: { id?: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
@ -43,12 +43,14 @@ export default function ProfilePage({ id }: { id?: string }) {
|
||||||
if (!profile && isFetching) {
|
if (!profile && isFetching) {
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout>
|
<SecondaryPageLayout>
|
||||||
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
<div className="max-sm:px-4">
|
||||||
<Skeleton className="w-full h-full object-cover rounded-lg" />
|
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
||||||
<Skeleton className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background rounded-full" />
|
<Skeleton className="w-full h-full object-cover rounded-lg" />
|
||||||
|
<Skeleton className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background rounded-full" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-5 w-28 mt-14 mb-1" />
|
||||||
|
<Skeleton className="h-5 w-56 mt-2 my-1 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-5 w-28 mt-14 mb-1" />
|
|
||||||
<Skeleton className="h-5 w-56 mt-2 my-1 rounded-full" />
|
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -57,44 +59,46 @@ export default function ProfilePage({ id }: { id?: string }) {
|
||||||
const { banner, username, nip05, about, avatar, pubkey } = profile
|
const { banner, username, nip05, about, avatar, pubkey } = profile
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout titlebarContent={username}>
|
<SecondaryPageLayout titlebarContent={username}>
|
||||||
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
<div className="max-sm:px-4">
|
||||||
<ProfileBanner
|
<div className="relative bg-cover bg-center w-full aspect-[21/9] rounded-lg mb-2">
|
||||||
banner={banner}
|
<ProfileBanner
|
||||||
pubkey={pubkey}
|
banner={banner}
|
||||||
className="w-full h-full object-cover rounded-lg"
|
pubkey={pubkey}
|
||||||
/>
|
className="w-full h-full object-cover rounded-lg"
|
||||||
<Avatar className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background">
|
/>
|
||||||
<AvatarImage src={avatar} className="object-cover object-center" />
|
<Avatar className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background">
|
||||||
<AvatarFallback>
|
<AvatarImage src={avatar} className="object-cover object-center" />
|
||||||
<img src={defaultImage} />
|
<AvatarFallback>
|
||||||
</AvatarFallback>
|
<img src={defaultImage} />
|
||||||
</Avatar>
|
</AvatarFallback>
|
||||||
</div>
|
</Avatar>
|
||||||
<div className="flex justify-end h-8 gap-2 items-center">
|
</div>
|
||||||
{isFollowingYou && (
|
<div className="flex justify-end h-8 gap-2 items-center">
|
||||||
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2">
|
{isFollowingYou && (
|
||||||
{t('Follows you')}
|
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2">
|
||||||
</div>
|
{t('Follows you')}
|
||||||
)}
|
</div>
|
||||||
<FollowButton pubkey={pubkey} />
|
)}
|
||||||
</div>
|
<FollowButton pubkey={pubkey} />
|
||||||
<div className="pt-2">
|
</div>
|
||||||
<div className="text-xl font-semibold">{username}</div>
|
<div className="pt-2">
|
||||||
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} />}
|
<div className="text-xl font-semibold">{username}</div>
|
||||||
<div className="flex gap-1 mt-1">
|
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} />}
|
||||||
<PubkeyCopy pubkey={pubkey} />
|
<div className="flex gap-1 mt-1">
|
||||||
<QrCodePopover pubkey={pubkey} />
|
<PubkeyCopy pubkey={pubkey} />
|
||||||
|
<QrCodePopover pubkey={pubkey} />
|
||||||
|
</div>
|
||||||
|
<ProfileAbout about={about} className="text-wrap break-words whitespace-pre-wrap mt-2" />
|
||||||
|
<SecondaryPageLink
|
||||||
|
to={toFollowingList(pubkey)}
|
||||||
|
className="mt-2 flex gap-1 hover:underline text-sm w-fit"
|
||||||
|
>
|
||||||
|
{isSelf ? selfFollowings.length : followings.length}
|
||||||
|
<div className="text-muted-foreground">{t('Following')}</div>
|
||||||
|
</SecondaryPageLink>
|
||||||
</div>
|
</div>
|
||||||
<ProfileAbout about={about} className="text-wrap break-words whitespace-pre-wrap mt-2" />
|
|
||||||
<SecondaryPageLink
|
|
||||||
to={toFollowingList(pubkey)}
|
|
||||||
className="mt-2 flex gap-1 hover:underline text-sm w-fit"
|
|
||||||
>
|
|
||||||
{isSelf ? selfFollowings.length : followings.length}
|
|
||||||
<div className="text-muted-foreground">{t('Following')}</div>
|
|
||||||
</SecondaryPageLink>
|
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-4" />
|
<Separator className="mt-4 sm:my-4" />
|
||||||
<NoteList
|
<NoteList
|
||||||
key={pubkey}
|
key={pubkey}
|
||||||
filter={{ authors: [pubkey] }}
|
filter={{ authors: [pubkey] }}
|
||||||
|
|
|
||||||
15
src/renderer/src/pages/secondary/RelaySettingsPage/index.tsx
Normal file
15
src/renderer/src/pages/secondary/RelaySettingsPage/index.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import RelaySettings from '@renderer/components/RelaySettings'
|
||||||
|
import SecondaryPageLayout from '@renderer/layouts/SecondaryPageLayout'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function RelaySettingsPage() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SecondaryPageLayout titlebarContent={t('Relay settings')}>
|
||||||
|
<div className="max-sm:px-4">
|
||||||
|
<RelaySettings hideTitle />
|
||||||
|
</div>
|
||||||
|
</SecondaryPageLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/renderer/src/providers/ScreenSizeProvider.tsx
Normal file
56
src/renderer/src/providers/ScreenSizeProvider.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
type TScreenSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
||||||
|
|
||||||
|
type TScreenSizeContext = {
|
||||||
|
screenSize: TScreenSize
|
||||||
|
isSmallScreen: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScreenSizeContext = createContext<TScreenSizeContext | undefined>(undefined)
|
||||||
|
|
||||||
|
export const useScreenSize = () => {
|
||||||
|
const context = useContext(ScreenSizeContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useScreenSize must be used within a ScreenSizeProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenSizeProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [screenSize, setScreenSize] = useState<TScreenSize>('xl')
|
||||||
|
const isSmallScreen = screenSize === 'sm'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (window.innerWidth < 640) {
|
||||||
|
setScreenSize('sm')
|
||||||
|
} else if (window.innerWidth < 768) {
|
||||||
|
setScreenSize('md')
|
||||||
|
} else if (window.innerWidth < 1024) {
|
||||||
|
setScreenSize('lg')
|
||||||
|
} else if (window.innerWidth < 1280) {
|
||||||
|
setScreenSize('xl')
|
||||||
|
} else {
|
||||||
|
setScreenSize('2xl')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenSizeContext.Provider
|
||||||
|
value={{
|
||||||
|
screenSize,
|
||||||
|
isSmallScreen
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScreenSizeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import NoteListPage from './pages/secondary/NoteListPage'
|
||||||
import NotePage from './pages/secondary/NotePage'
|
import NotePage from './pages/secondary/NotePage'
|
||||||
import ProfileListPage from './pages/secondary/ProfileListPage'
|
import ProfileListPage from './pages/secondary/ProfileListPage'
|
||||||
import ProfilePage from './pages/secondary/ProfilePage'
|
import ProfilePage from './pages/secondary/ProfilePage'
|
||||||
|
import RelaySettingsPage from './pages/secondary/RelaySettingsPage'
|
||||||
|
|
||||||
const ROUTES = [
|
const ROUTES = [
|
||||||
{ path: '/', element: <HomePage /> },
|
{ path: '/', element: <HomePage /> },
|
||||||
|
|
@ -13,7 +14,8 @@ const ROUTES = [
|
||||||
{ path: '/note/:id', element: <NotePage /> },
|
{ path: '/note/:id', element: <NotePage /> },
|
||||||
{ path: '/user', element: <ProfileListPage /> },
|
{ path: '/user', element: <ProfileListPage /> },
|
||||||
{ path: '/user/:id', element: <ProfilePage /> },
|
{ path: '/user/:id', element: <ProfilePage /> },
|
||||||
{ path: '/user/:id/following', element: <FollowingListPage /> }
|
{ path: '/user/:id/following', element: <FollowingListPage /> },
|
||||||
|
{ path: '/relay-settings', element: <RelaySettingsPage /> }
|
||||||
]
|
]
|
||||||
|
|
||||||
export const routes = ROUTES.map(({ path, element }) => ({
|
export const routes = ROUTES.map(({ path, element }) => ({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue