Browse Source

move ? button on mobile

make carousel buttons bigger
imwald
Silberengel 2 weeks ago
parent
commit
f7122baae6
  1. 2
      src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx
  2. 57
      src/components/HelpAndAccountMenu.tsx
  3. 126
      src/components/LiveActivitiesStrip.tsx
  4. 2
      src/i18n/locales/de.ts
  5. 2
      src/i18n/locales/en.ts

2
src/components/ConnectedRelays/ActiveRelaysTitlebarButton.tsx

@ -50,7 +50,7 @@ export function ActiveRelaysTitlebarButton() { @@ -50,7 +50,7 @@ export function ActiveRelaysTitlebarButton() {
<Button
variant="ghost"
size="titlebar-icon"
className="shrink-0 gap-0.5 text-muted-foreground hover:text-primary disabled:opacity-40"
className="shrink-0 gap-0.5 text-muted-foreground hover:text-primary disabled:opacity-40 max-sm:mr-3 max-sm:pr-1"
title={t('Active relays')}
aria-label={t('Active relays')}
disabled={rows.length === 0}

57
src/components/HelpAndAccountMenu.tsx

@ -1,6 +1,5 @@ @@ -1,6 +1,5 @@
import LoginDialog from '@/components/LoginDialog'
import LogoutDialog from '@/components/LogoutDialog'
import { KeyboardShortcutsHelpButton } from '@/components/KeyboardShortcutsHelp'
import KeyboardShortcutsHelpSidebarButton from '@/components/Sidebar/KeyboardShortcutsHelpSidebarButton'
import SidebarItem from '@/components/Sidebar/SidebarItem'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
@ -17,10 +16,11 @@ import { formatPubkey, formatNpub, generateImageByPubkey, pubkeyToNpub } from '@ @@ -17,10 +16,11 @@ import { formatPubkey, formatNpub, generateImageByPubkey, pubkeyToNpub } from '@
import { isVideo } from '@/lib/url'
import { cn } from '@/lib/utils'
import { useCacheBrowser } from '../contexts/cache-browser-context'
import { useKeyboardShortcutsHelp } from '@/contexts/keyboard-shortcuts-help-context'
import { usePrimaryPage } from '@/contexts/primary-page-context'
import { useFetchProfile } from '@/hooks/useFetchProfile'
import { useNostr } from '@/providers/NostrProvider'
import { ArrowDownUp, Database, LogIn, LogOut, Settings, User, UserRound } from 'lucide-react'
import { ArrowDownUp, CircleHelp, Database, LogIn, LogOut, Settings, User, UserRound } from 'lucide-react'
import { useMemo, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
@ -28,17 +28,34 @@ export type HelpAndAccountMenuVariant = 'sidebar' | 'titlebar' @@ -28,17 +28,34 @@ export type HelpAndAccountMenuVariant = 'sidebar' | 'titlebar'
function AccountDropdownItems({
onSwitchAccount,
onLogoutClick
onLogoutClick,
includeHelp
}: {
onSwitchAccount: () => void
onLogoutClick: () => void
/** Titlebar (mobile): help lives here so the relay strip has more room. */
includeHelp?: boolean
}) {
const { t } = useTranslation()
const { navigate } = usePrimaryPage()
const { openBrowseCache } = useCacheBrowser()
const { openHelp } = useKeyboardShortcutsHelp()
return (
<>
{includeHelp ? (
<>
<DropdownMenuItem
onClick={() => {
openHelp()
}}
>
<CircleHelp className="size-4" />
{t('help.title')}
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
) : null}
<DropdownMenuItem onClick={() => navigate('profile')}>
<User className="size-4" />
{t('Profile')}
@ -120,7 +137,11 @@ function SidebarAccountMenu({ @@ -120,7 +137,11 @@ function SidebarAccountMenu({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="end" className="z-[220]">
<AccountDropdownItems onSwitchAccount={onSwitchAccount} onLogoutClick={onLogoutClick} />
<AccountDropdownItems
onSwitchAccount={onSwitchAccount}
onLogoutClick={onLogoutClick}
includeHelp={false}
/>
</DropdownMenuContent>
</DropdownMenu>
)
@ -174,23 +195,27 @@ function TitlebarAccountMenu({ @@ -174,23 +195,27 @@ function TitlebarAccountMenu({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" className="z-[220]">
<AccountDropdownItems onSwitchAccount={onSwitchAccount} onLogoutClick={onLogoutClick} />
<AccountDropdownItems
onSwitchAccount={onSwitchAccount}
onLogoutClick={onLogoutClick}
includeHelp
/>
</DropdownMenuContent>
</DropdownMenu>
)
}
/**
* Help (?) + account avatar with the same dropdown on sidebar (desktop) and titlebar (mobile).
* Sidebar: help (?) above account. Titlebar (mobile): help is inside the account menu so the relay strip has more room.
*/
export default function HelpAndAccountMenu({ variant }: { variant: HelpAndAccountMenuVariant }) {
const { t } = useTranslation()
const { pubkey, checkLogin } = useNostr()
const { openHelp } = useKeyboardShortcutsHelp()
const [loginDialogOpen, setLoginDialogOpen] = useState(false)
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false)
const help =
variant === 'sidebar' ? <KeyboardShortcutsHelpSidebarButton /> : <KeyboardShortcutsHelpButton />
const help = variant === 'sidebar' ? <KeyboardShortcutsHelpSidebarButton /> : null
let account: ReactNode
if (pubkey) {
@ -223,10 +248,26 @@ export default function HelpAndAccountMenu({ variant }: { variant: HelpAndAccoun @@ -223,10 +248,26 @@ export default function HelpAndAccountMenu({ variant }: { variant: HelpAndAccoun
const wrapClass =
variant === 'titlebar' ? 'flex shrink-0 items-center gap-1' : 'flex flex-col space-y-2'
/** Logged-out titlebar: keep ? next to login so help stays reachable without opening login. */
const titlebarHelpWhenLoggedOut =
variant === 'titlebar' && !pubkey ? (
<Button
type="button"
variant="ghost"
size="titlebar-icon"
onClick={() => openHelp()}
title={t('help.title')}
aria-label={t('help.title')}
>
<CircleHelp />
</Button>
) : null
return (
<>
<div className={wrapClass}>
{help}
{titlebarHelpWhenLoggedOut}
{account}
</div>
<LoginDialog open={loginDialogOpen} setOpen={setLoginDialogOpen} />

126
src/components/LiveActivitiesStrip.tsx

@ -6,11 +6,15 @@ import { useLiveActivitiesOptional } from '@/providers/useLiveActivities' @@ -6,11 +6,15 @@ import { useLiveActivitiesOptional } from '@/providers/useLiveActivities'
import { useUserPreferencesOptional } from '@/providers/UserPreferencesProvider'
import storage from '@/services/local-storage.service'
import { ExternalLink } from 'lucide-react'
import { useCallback, useEffect, useLayoutEffect, useState } from 'react'
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
type TPlacement = 'sidebar' | 'mobile'
const SWIPE_MIN_PX = 44
const SWIPE_DOMINANCE_RATIO = 1.2
const SWIPE_NOTE_OPEN_SUPPRESS_MS = 400
export default function LiveActivitiesStrip({ placement }: { placement: TPlacement }) {
const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigation()
@ -48,11 +52,61 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme @@ -48,11 +52,61 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
setSlide((s) => Math.min(s, items.length - 1))
}, [items.length])
const swipeHintId = useId()
const swipeGrabRef = useRef<{ x: number; y: number; pointerId: number } | null>(null)
const swipeNavAtRef = useRef(0)
const releasePointerIfCaptured = useCallback((el: HTMLDivElement, pointerId: number) => {
if (typeof el.hasPointerCapture === 'function' && el.hasPointerCapture(pointerId)) {
el.releasePointerCapture(pointerId)
}
}, [])
const onSwipePointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
if (placement !== 'mobile' || items.length <= 1) return
swipeGrabRef.current = { x: e.clientX, y: e.clientY, pointerId: e.pointerId }
e.currentTarget.setPointerCapture(e.pointerId)
},
[placement, items.length]
)
const onSwipePointerUp = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
if (placement !== 'mobile' || items.length <= 1) return
const grab = swipeGrabRef.current
swipeGrabRef.current = null
releasePointerIfCaptured(e.currentTarget, e.pointerId)
if (!grab || grab.pointerId !== e.pointerId) return
const dx = e.clientX - grab.x
const dy = e.clientY - grab.y
const ax = Math.abs(dx)
const ay = Math.abs(dy)
if (ax < SWIPE_MIN_PX || ax < ay * SWIPE_DOMINANCE_RATIO) return
swipeNavAtRef.current = Date.now()
if (dx < 0) {
setSlide((s) => (s + 1) % items.length)
} else {
setSlide((s) => (s - 1 + items.length) % items.length)
}
},
[placement, items.length, releasePointerIfCaptured]
)
const onSwipePointerCancel = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
swipeGrabRef.current = null
releasePointerIfCaptured(e.currentTarget, e.pointerId)
},
[releasePointerIfCaptured]
)
// `items` can shrink without a new array identity; `slide` may then be out of range until effects run.
const displayIndex = items.length === 0 ? 0 : Math.min(slide, items.length - 1)
const itemAtSlide = items[displayIndex]
const openLiveNote = useCallback(() => {
if (Date.now() - swipeNavAtRef.current < SWIPE_NOTE_OPEN_SUPPRESS_MS) return
const ev = itemAtSlide?.event
if (!ev) return
// Same bech32 as {@link getNoteBech32Id} + pass event so {@link navigationEventStore} / cache match the URL.
@ -68,16 +122,25 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme @@ -68,16 +122,25 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
return null
}
const mobileSwipe = placement === 'mobile' && items.length > 1
return (
<div
className={cn(
'min-w-0 max-w-full overflow-hidden',
placement === 'sidebar' &&
'mb-2 rounded-lg border border-border/80 bg-muted/50 p-2 shadow-sm dark:bg-muted/30',
placement === 'mobile' && 'w-full shrink-0 border-b border-border/80 bg-muted/50 px-2 py-2 dark:bg-muted/30'
)}
role="region"
aria-label={t('liveActivities.regionLabel')}
aria-describedby={mobileSwipe ? swipeHintId : undefined}
>
{mobileSwipe ? (
<span id={swipeHintId} className="sr-only">
{t('liveActivities.swipeToBrowse')}
</span>
) : null}
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground xl:text-xs">
{t('liveActivities.heading')}
</div>
@ -85,8 +148,12 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme @@ -85,8 +148,12 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
className={cn(
'flex min-w-0 gap-1.5 rounded-md',
placement === 'sidebar' && 'flex-col xl:flex-row xl:items-stretch',
placement === 'mobile' && 'items-stretch'
placement === 'mobile' && 'items-stretch touch-pan-y',
mobileSwipe && 'cursor-grab active:cursor-grabbing'
)}
onPointerDown={mobileSwipe ? onSwipePointerDown : undefined}
onPointerUp={mobileSwipe ? onSwipePointerUp : undefined}
onPointerCancel={mobileSwipe ? onSwipePointerCancel : undefined}
>
<button
type="button"
@ -134,23 +201,44 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme @@ -134,23 +201,44 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
</a>
</div>
{items.length > 1 ? (
<div className="mt-2 flex justify-center gap-1.5">
{items.map((item, i) => (
<button
key={item.address}
type="button"
aria-label={t('liveActivities.goToSlide', { n: i + 1 })}
className={cn(
'size-1.5 rounded-full transition-colors',
i === displayIndex ? 'bg-primary' : 'bg-muted-foreground/40 hover:bg-muted-foreground/60'
)}
onClick={(e) => {
e.preventDefault()
setSlide(i)
}}
/>
))}
</div>
placement === 'mobile' ? (
<div className="mt-2 flex justify-center gap-1.5" aria-hidden>
{items.map((item, i) => (
<span
key={item.address}
className={cn(
'block size-1.5 rounded-full',
i === displayIndex ? 'bg-primary' : 'bg-muted-foreground/45'
)}
/>
))}
</div>
) : (
<div className="mt-2 flex justify-center gap-1.5" role="tablist">
{items.map((item, i) => (
<button
key={item.address}
type="button"
role="tab"
aria-selected={i === displayIndex}
aria-label={t('liveActivities.goToSlide', { n: i + 1 })}
className="flex shrink-0 items-center justify-center rounded-full p-0.5 active:opacity-80"
onClick={(e) => {
e.preventDefault()
setSlide(i)
}}
>
<span
className={cn(
'block size-1.5 rounded-full transition-colors',
i === displayIndex ? 'bg-primary' : 'bg-muted-foreground/45 hover:bg-muted-foreground/70'
)}
aria-hidden
/>
</button>
))}
</div>
)
) : null}
</div>
)

2
src/i18n/locales/de.ts

@ -621,6 +621,8 @@ export default { @@ -621,6 +621,8 @@ export default {
'liveActivities.regionLabel': 'Live-Räume und Streams',
'liveActivities.fromFollow': 'Von jemandem, dem du folgst',
'liveActivities.goToSlide': 'Live-Eintrag {{n}} anzeigen',
'liveActivities.swipeToBrowse':
'Wische auf dem Banner nach links oder rechts, um zwischen Live-Aktivitäten zu wechseln.',
'liveActivities.viewNoteTitle':
'Diese Live-Aktivität als Beitrag öffnen (Wiedergabe in der App, Links darunter)',
'liveActivities.openJoinPageTitle':

2
src/i18n/locales/en.ts

@ -616,6 +616,8 @@ export default { @@ -616,6 +616,8 @@ export default {
'liveActivities.regionLabel': 'Live spaces and streams',
'liveActivities.fromFollow': 'From someone you follow',
'liveActivities.goToSlide': 'Show live item {{n}}',
'liveActivities.swipeToBrowse':
'Swipe left or right on the banner to switch between live activities.',
'liveActivities.viewNoteTitle': 'Open this live activity as a note (play in app, links below)',
'liveActivities.openJoinPageTitle': 'Open the join page in a new tab (e.g. zap.stream or the room site)',
'liveActivities.settingsToggle': 'Live activities banner',

Loading…
Cancel
Save