Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
f3496f2968
  1. 178
      src/components/CalendarEventContent/index.tsx
  2. 49
      src/components/Embedded/EmbeddedCalendarEvent.tsx
  3. 6
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  4. 4
      src/components/Note/index.tsx
  5. 32
      src/components/Sidebar/SidebarCalendarWeekWidget.tsx
  6. 2
      src/i18n/locales/cs.ts
  7. 2
      src/i18n/locales/de.ts
  8. 2
      src/i18n/locales/en.ts
  9. 2
      src/i18n/locales/es.ts
  10. 2
      src/i18n/locales/fr.ts
  11. 2
      src/i18n/locales/nl.ts
  12. 2
      src/i18n/locales/pl.ts
  13. 2
      src/i18n/locales/ru.ts
  14. 2
      src/i18n/locales/tr.ts
  15. 2
      src/i18n/locales/zh.ts
  16. 42
      src/lib/calendar-event.ts
  17. 54
      src/pages/primary/CalendarPrimaryPage.tsx
  18. 16
      src/pages/secondary/CalendarDayEventsPage/index.tsx

178
src/components/CalendarEventContent/index.tsx

@ -10,12 +10,13 @@ import { useFetchCalendarRsvps } from '@/hooks/useFetchCalendarRsvps' @@ -10,12 +10,13 @@ import { useFetchCalendarRsvps } from '@/hooks/useFetchCalendarRsvps'
import { useNostr } from '@/providers/NostrProvider'
import { toProfile } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager'
import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
import { useMemo } from 'react'
import Collapsible from '../Collapsible'
import { Button } from '../ui/button'
import { Calendar, Clock, Video, CheckCircle, HelpCircle, XCircle } from 'lucide-react'
import { Calendar, Clock, ExternalLink, MapPin, CheckCircle, HelpCircle, XCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
import {
DropdownMenu,
@ -32,22 +33,44 @@ type RsvpStatus = 'accepted' | 'tentative' | 'declined' @@ -32,22 +33,44 @@ type RsvpStatus = 'accepted' | 'tentative' | 'declined'
export default function CalendarEventContent({
event,
className,
showRsvp = true
showRsvp = true,
showFull = false
}: {
event: Event
className?: string
showRsvp?: boolean
/** Note page / full detail: markdown body + complete tag list. */
showFull?: boolean
}) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { pubkey: myPubkey, publish } = useNostr()
const { rsvps, isFetching, getRsvpStatus: getStatus } = useFetchCalendarRsvps(event)
if (!isCalendarEventKind(event.kind)) return null
const meta = useMemo(() => {
if (!isCalendarEventKind(event.kind)) return null
return getCalendarEventMeta(event)
}, [event])
const markdownBody = useMemo(() => {
if (!meta) return ''
const s = meta.summary.trim()
const c = event.content?.trim() ?? ''
if (s && c) return `${s}\n\n${c}`
return s || c || ''
}, [meta, event.content])
const eventForMarkdown = useMemo((): Event => {
if (!markdownBody) return event
return { ...event, content: markdownBody }
}, [event, markdownBody])
const duplicateWebPreviewHints = useMemo(
() =>
!meta ? [] : [...meta.rUrls, ...(meta.image?.trim() ? [meta.image.trim()] : [])],
[meta]
)
const { title, summary, image, start, end, startDate, endDate, isDateBased, joinUrl, topics } =
getCalendarEventMeta(event)
const description = summary || event.content?.trim() || ''
const myRsvp = myPubkey ? rsvps.find((r) => r.pubkey === myPubkey) : undefined
const myStatus = myRsvp ? getStatus(myRsvp) : undefined
@ -71,6 +94,23 @@ export default function CalendarEventContent({ @@ -71,6 +94,23 @@ export default function CalendarEventContent({
})
}, [event.pubkey, event.tags, rsvps])
if (!meta) return null
const {
title,
image,
start,
end,
startDate,
endDate,
isDateBased,
rUrls,
location,
startTzid,
endTzid,
topics
} = meta
const handleRsvp = async (status: RsvpStatus) => {
if (!myPubkey) {
toast.error(t('You need to log in to RSVP'))
@ -105,17 +145,47 @@ export default function CalendarEventContent({ @@ -105,17 +145,47 @@ export default function CalendarEventContent({
<img
src={image}
alt=""
className="size-[4.5rem] shrink-0 rounded-lg object-cover shadow-sm ring-1 ring-border/40"
loading="lazy"
referrerPolicy="no-referrer"
className={cn(
'shrink-0 rounded-lg object-cover shadow-sm ring-1 ring-border/40',
showFull ? 'size-[4.5rem]' : 'size-10'
)}
/>
) : (
<div className="flex size-[4.5rem] shrink-0 items-center justify-center rounded-lg bg-primary/10 ring-1 ring-border/40">
<Calendar className="size-7 text-primary/80" aria-hidden />
<div
className={cn(
'flex shrink-0 items-center justify-center rounded-lg bg-primary/10 ring-1 ring-border/40',
showFull ? 'size-[4.5rem]' : 'size-10'
)}
>
<Calendar
className={cn('text-primary/80', showFull ? 'size-7' : 'size-5')}
aria-hidden
/>
</div>
)}
<div className="min-w-0 flex-1 space-y-2">
<h3 className="text-lg font-semibold leading-snug tracking-tight text-foreground">
<h3
className={cn(
'font-semibold leading-snug tracking-tight text-foreground',
showFull ? 'text-lg' : 'line-clamp-2 text-base'
)}
>
{title || t('Scheduled video call')}
</h3>
{!showFull && scheduleLine ? (
<p className="flex items-start gap-1.5 text-xs font-medium leading-snug text-foreground">
<Clock className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" aria-hidden />
<span className="min-w-0">{scheduleLine}</span>
</p>
) : null}
{!showFull && location ? (
<p className="flex items-start gap-1.5 text-xs leading-snug text-muted-foreground">
<MapPin className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" aria-hidden />
<span className="min-w-0 line-clamp-3">{location}</span>
</p>
) : null}
{topics.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{topics.map((topic) => (
@ -130,31 +200,66 @@ export default function CalendarEventContent({ @@ -130,31 +200,66 @@ export default function CalendarEventContent({
)}
</div>
</div>
{scheduleLine ? (
{showFull && scheduleLine ? (
<div className="flex gap-2 rounded-lg border border-border/60 bg-background/60 px-3 py-2.5">
<Clock className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden />
<p className="min-w-0 text-sm font-medium leading-snug text-foreground">{scheduleLine}</p>
<div className="min-w-0 flex-1 space-y-1">
<p className="text-sm font-medium leading-snug text-foreground">{scheduleLine}</p>
{(startTzid || endTzid) && (
<p className="text-xs leading-snug text-muted-foreground">
{startTzid ? (
<>
<span className="font-medium text-foreground/80">start_tzid</span>: {startTzid}
</>
) : null}
{startTzid && endTzid && endTzid !== startTzid ? ' · ' : ''}
{endTzid && endTzid !== startTzid ? (
<>
<span className="font-medium text-foreground/80">end_tzid</span>: {endTzid}
</>
) : null}
</p>
)}
</div>
</div>
) : null}
{description ? (
<>
{/* NIP-52 31922/31923: collapse long summary+body only; card chrome stays outside MainNoteCard Collapsible. */}
<Collapsible threshold={200} collapsedHeight={160} className="min-w-0">
<p className="whitespace-pre-wrap break-words text-sm leading-relaxed text-muted-foreground">
{description}
</p>
</Collapsible>
</>
{showFull && location ? (
<div className="flex gap-2 rounded-lg border border-border/60 bg-background/40 px-3 py-2.5">
<MapPin className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden />
<p className="min-w-0 text-sm leading-snug text-foreground">{location}</p>
</div>
) : null}
{markdownBody ? (
showFull ? (
<div className="not-prose min-w-0 border-t border-border/50 pt-3" data-calendar-event-markdown>
<MarkdownArticle
event={eventForMarkdown}
className="prose-sm"
hideMetadata
lazyMedia={false}
duplicateWebPreviewCleanedUrlHints={duplicateWebPreviewHints}
/>
</div>
) : (
<>
{/* NIP-52 31922/31923: collapse long summary+body only; card chrome stays outside MainNoteCard Collapsible. */}
<Collapsible threshold={200} collapsedHeight={160} className="min-w-0">
<p className="whitespace-pre-wrap break-words text-sm leading-relaxed text-muted-foreground">
{markdownBody}
</p>
</Collapsible>
</>
)
) : null}
<div className="flex flex-wrap items-center gap-2 pt-0.5">
{joinUrl && (
<Button variant="secondary" size="sm" className="gap-2" asChild>
<a href={joinUrl} target="_blank" rel="noopener noreferrer">
<Video className="size-4" />
{t('Join video call')}
{rUrls.map((url) => (
<Button key={url} variant="secondary" size="sm" className="gap-2" asChild>
<a href={url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="size-4 shrink-0" />
{t('Open link')}
</a>
</Button>
)}
))}
{showRsvp && myPubkey && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -245,6 +350,25 @@ export default function CalendarEventContent({ @@ -245,6 +350,25 @@ export default function CalendarEventContent({
</ul>
</div>
)}
{showFull && event.tags.length > 0 ? (
<div className="border-t border-border/50 pt-3">
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{t('All tags')}
</div>
<dl className="min-w-0 space-y-2">
{event.tags.map((tag, idx) => (
<div key={`${tag[0]}-${idx}`} className="grid gap-1 sm:grid-cols-[minmax(0,7rem)_1fr] sm:gap-3">
<dt className="font-mono text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
{tag[0] || '—'}
</dt>
<dd className="min-w-0 break-all text-xs text-foreground">
{tag.length > 1 ? tag.slice(1).join(' · ') : '—'}
</dd>
</div>
))}
</dl>
</div>
) : null}
</div>
)
}

49
src/components/Embedded/EmbeddedCalendarEvent.tsx

@ -9,7 +9,7 @@ import { Event } from 'nostr-tools' @@ -9,7 +9,7 @@ import { Event } from 'nostr-tools'
import { useTranslation } from 'react-i18next'
import Collapsible from '../Collapsible'
import { Button } from '../ui/button'
import { Calendar, Clock, Video } from 'lucide-react'
import { Calendar, Clock, ExternalLink, MapPin } from 'lucide-react'
export function EmbeddedCalendarEvent({
event,
@ -20,7 +20,7 @@ export function EmbeddedCalendarEvent({ @@ -20,7 +20,7 @@ export function EmbeddedCalendarEvent({
}) {
const { t } = useTranslation()
if (!isCalendarEventKind(event.kind)) return null
const { title, summary, image, start, end, startDate, endDate, isDateBased, joinUrl, topics } =
const { title, summary, image, start, end, startDate, endDate, isDateBased, rUrls, topics, location } =
getCalendarEventMeta(event)
const description = summary || event.content?.trim() || ''
@ -44,17 +44,31 @@ export function EmbeddedCalendarEvent({ @@ -44,17 +44,31 @@ export function EmbeddedCalendarEvent({
<img
src={image}
alt=""
className="size-14 shrink-0 rounded-lg object-cover ring-1 ring-border/40"
loading="lazy"
referrerPolicy="no-referrer"
className="size-10 shrink-0 rounded-md object-cover ring-1 ring-border/40"
/>
) : (
<div className="flex size-14 shrink-0 items-center justify-center rounded-lg bg-primary/10 ring-1 ring-border/40">
<Calendar className="size-6 text-primary/80" aria-hidden />
<div className="flex size-10 shrink-0 items-center justify-center rounded-md bg-primary/10 ring-1 ring-border/40">
<Calendar className="size-5 text-primary/80" aria-hidden />
</div>
)}
<div className="min-w-0 flex-1 space-y-1.5">
<span className="block truncate font-semibold leading-snug text-foreground">
<span className="block line-clamp-2 font-semibold leading-snug text-foreground">
{title || t('Scheduled video call')}
</span>
{scheduleLine ? (
<p className="flex items-start gap-1.5 text-[11px] font-medium leading-snug text-foreground">
<Clock className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" aria-hidden />
<span className="min-w-0">{scheduleLine}</span>
</p>
) : null}
{location ? (
<p className="flex items-start gap-1.5 text-[11px] leading-snug text-muted-foreground">
<MapPin className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" aria-hidden />
<span className="min-w-0 line-clamp-3">{location}</span>
</p>
) : null}
{topics.length > 0 && (
<div className="flex flex-wrap gap-1">
{topics.map((topic) => (
@ -69,12 +83,6 @@ export function EmbeddedCalendarEvent({ @@ -69,12 +83,6 @@ export function EmbeddedCalendarEvent({
)}
</div>
</div>
{scheduleLine ? (
<div className="flex gap-2 rounded-md border border-border/60 bg-background/60 px-2.5 py-2">
<Clock className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" aria-hidden />
<p className="min-w-0 text-xs font-medium leading-snug text-foreground">{scheduleLine}</p>
</div>
) : null}
{description ? (
<>
{/* NIP-52 31922/31923 embedded preview: long description only. */}
@ -85,19 +93,14 @@ export function EmbeddedCalendarEvent({ @@ -85,19 +93,14 @@ export function EmbeddedCalendarEvent({
</Collapsible>
</>
) : null}
{joinUrl && (
<Button
variant="secondary"
size="sm"
className="w-full gap-2 mt-1"
asChild
>
<a href={joinUrl} target="_blank" rel="noopener noreferrer">
<Video className="size-4" />
{t('Join video call')}
{rUrls.map((url) => (
<Button key={url} variant="secondary" size="sm" className="w-full gap-2 mt-1" asChild>
<a href={url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="size-4 shrink-0" />
{t('Open link')}
</a>
</Button>
)}
))}
</div>
)
}

6
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -28,6 +28,7 @@ import { Event, kinds } from 'nostr-tools' @@ -28,6 +28,7 @@ import { Event, kinds } from 'nostr-tools'
import Emoji, { EMOJI_IMG_INLINE_CLASS } from '@/components/Emoji'
import {
ExtendedKind,
isNip52CalendarCardKind,
SPOTIFY_OPEN_URL_REGEX,
WS_URL_REGEX,
YOUTUBE_URL_REGEX,
@ -5898,7 +5899,10 @@ export default function MarkdownArticle({ @@ -5898,7 +5899,10 @@ export default function MarkdownArticle({
<p className="break-words">{metadata.summary}</p>
</blockquote>
)}
{hideMetadata && metadata.title && event.kind !== ExtendedKind.DISCUSSION && (
{hideMetadata &&
metadata.title &&
event.kind !== ExtendedKind.DISCUSSION &&
!isNip52CalendarCardKind(event.kind) && (
<h2 className="text-2xl font-bold mb-4 leading-tight break-words">{metadata.title}</h2>
)}

4
src/components/Note/index.tsx

@ -396,7 +396,9 @@ export default function Note({ @@ -396,7 +396,9 @@ export default function Note({
} else if (event.kind === ExtendedKind.RELAY_REVIEW) {
content = <RelayReview className="mt-2" event={displayEvent} />
} else if (isCalendarEventKind(event.kind)) {
content = <CalendarEventContent event={displayEvent} className="mt-2" showRsvp />
content = (
<CalendarEventContent event={displayEvent} className="mt-2" showRsvp showFull={showFull} />
)
} else if (event.kind === ExtendedKind.PUBLIC_MESSAGE) {
content = renderEventContent({ hideMetadata: true })
} else if (event.kind === ExtendedKind.ZAP_REQUEST || event.kind === ExtendedKind.ZAP_RECEIPT) {

32
src/components/Sidebar/SidebarCalendarWeekWidget.tsx

@ -2,6 +2,7 @@ import { @@ -2,6 +2,7 @@ import {
calendarOccurrenceOverlapsRange,
formatCalendarSidebarRow,
formatSidebarWeekLabel,
getCalendarEventMeta,
getCalendarOccurrenceWindowMs,
getLocalMondayWeekBounds
} from '@/lib/calendar-event'
@ -262,7 +263,9 @@ export default function SidebarCalendarWeekWidget() { @@ -262,7 +263,9 @@ export default function SidebarCalendarWeekWidget() {
) : (
<ul className="min-w-0 space-y-1 overflow-y-auto pr-0.5" style={{ maxHeight: LIST_MAX_HEIGHT_PX }}>
{sortedForWeek.map((ev) => {
const title = ev.tags.find((t) => t[0] === 'title')?.[1]?.trim() || t('Scheduled video call')
const meta = getCalendarEventMeta(ev)
const title = meta.title?.trim() || t('Scheduled video call')
const cover = meta.image?.trim()
const sub = formatCalendarSidebarRow(ev)
return (
<li key={replaceableEventDedupeKey(ev)}>
@ -270,16 +273,29 @@ export default function SidebarCalendarWeekWidget() { @@ -270,16 +273,29 @@ export default function SidebarCalendarWeekWidget() {
type="button"
onClick={() => openEvent(ev)}
className={cn(
'w-full rounded-md border border-transparent px-1.5 py-1.5 text-left transition-colors',
'flex w-full gap-2 rounded-md border border-transparent px-1.5 py-1.5 text-left transition-colors',
'hover:border-border/80 hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
)}
>
<span className="line-clamp-2 text-[11px] font-medium leading-snug text-foreground">{title}</span>
{sub ? (
<span className="mt-0.5 block line-clamp-2 text-[10px] leading-snug text-muted-foreground">
{sub}
</span>
) : null}
{cover ? (
<img
src={cover}
alt=""
loading="lazy"
referrerPolicy="no-referrer"
className="size-8 shrink-0 rounded-md object-cover ring-1 ring-border/40"
/>
) : (
<div className="size-8 shrink-0 rounded-md bg-muted/50 ring-1 ring-border/30" aria-hidden />
)}
<span className="min-w-0 flex-1">
<span className="line-clamp-2 text-[11px] font-medium leading-snug text-foreground">{title}</span>
{sub ? (
<span className="mt-0.5 block line-clamp-2 text-[10px] leading-snug text-muted-foreground">
{sub}
</span>
) : null}
</span>
</button>
</li>
)

2
src/i18n/locales/cs.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/de.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Mindestens eine Person hinzufügen (nostr:npub- oder nostr:nprofile-Links einfügen)",
"Scheduled call created and {{count}} invite(s) sent": "Geplanter Anruf erstellt und {{count}} Einladung(en) gesendet",
"Join video call": "Videoanruf beitreten",
"Open link": "Link öffnen",
"All tags": "Alle Tags",
"Scheduled video call": "Geplanter Videoanruf",
"Video call": "Videoanruf",
"Schedule and send invite": "Planen und Einladung senden",

2
src/i18n/locales/en.ts

@ -232,6 +232,8 @@ export default { @@ -232,6 +232,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/es.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/fr.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/nl.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/pl.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/ru.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/tr.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

2
src/i18n/locales/zh.ts

@ -228,6 +228,8 @@ export default { @@ -228,6 +228,8 @@ export default {
"Add at least one invitee (paste nostr:npub or nostr:nprofile links)": "Add at least one invitee (paste nostr:npub or nostr:nprofile links)",
"Scheduled call created and {{count}} invite(s) sent": "Scheduled call created and {{count}} invite(s) sent",
"Join video call": "Join video call",
"Open link": "Open link",
"All tags": "All tags",
"Scheduled video call": "Scheduled video call",
"Video call": "Video call",
"Schedule and send invite": "Schedule and send invite",

42
src/lib/calendar-event.ts

@ -15,7 +15,21 @@ export interface CalendarEventMeta { @@ -15,7 +15,21 @@ export interface CalendarEventMeta {
/** Date-based: YYYY-MM-DD (exclusive end). Time-based: undefined. */
endDate: string
isDateBased: boolean
/** First `r` tag with an http(s) URL (join / registration link). */
joinUrl: string
/** Same as {@link joinUrl}; every http(s) `r` value. */
rUrl: string
rUrls: string[]
/** `location` tag (venue / address text). */
location: string
/** `d` tag (replaceable identifier). */
d: string
/** `g` tag (geohash). */
geo: string
/** `start_tzid` (IANA zone). */
startTzid: string
/** `end_tzid` (IANA zone). */
endTzid: string
topics: string[]
}
@ -25,9 +39,17 @@ export function getCalendarEventMeta(event: Event): CalendarEventMeta { @@ -25,9 +39,17 @@ export function getCalendarEventMeta(event: Event): CalendarEventMeta {
const image = event.tags.find(tagNameEquals('image'))?.[1] ?? ''
const startStr = event.tags.find(tagNameEquals('start'))?.[1]
const endStr = event.tags.find(tagNameEquals('end'))?.[1]
const location = event.tags.find(tagNameEquals('location'))?.[1]
const rTag = event.tags.find(tagNameEquals('r'))?.[1]
const joinUrl = rTag || location || ''
const location = event.tags.find(tagNameEquals('location'))?.[1] ?? ''
const d = event.tags.find(tagNameEquals('d'))?.[1] ?? ''
const geo = event.tags.find(tagNameEquals('g'))?.[1] ?? ''
const startTzid = event.tags.find(tagNameEquals('start_tzid'))?.[1] ?? ''
const endTzid = event.tags.find(tagNameEquals('end_tzid'))?.[1] ?? ''
const rUrls = event.tags
.filter(tagNameEquals('r'))
.map((t) => t[1]?.trim())
.filter((u): u is string => !!u && (u.startsWith('http://') || u.startsWith('https://')))
const rUrl = rUrls[0] ?? ''
const joinUrl = rUrl
const topics = event.tags.filter(tagNameEquals('t')).map((t) => t[1]?.trim()).filter(Boolean)
const isDateBased = event.kind === ExtendedKind.CALENDAR_EVENT_DATE
if (isDateBased) {
@ -41,6 +63,13 @@ export function getCalendarEventMeta(event: Event): CalendarEventMeta { @@ -41,6 +63,13 @@ export function getCalendarEventMeta(event: Event): CalendarEventMeta {
endDate: endStr ?? '',
isDateBased: true,
joinUrl,
rUrl,
rUrls,
location,
d,
geo,
startTzid,
endTzid,
topics
}
}
@ -56,6 +85,13 @@ export function getCalendarEventMeta(event: Event): CalendarEventMeta { @@ -56,6 +85,13 @@ export function getCalendarEventMeta(event: Event): CalendarEventMeta {
endDate: '',
isDateBased: false,
joinUrl,
rUrl,
rUrls,
location,
d,
geo,
startTzid,
endTzid,
topics
}
}

54
src/pages/primary/CalendarPrimaryPage.tsx

@ -19,11 +19,12 @@ import { useNostr } from '@/providers/NostrProvider' @@ -19,11 +19,12 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants'
import { TPageRef } from '@/types'
import { RefreshButton } from '@/components/RefreshButton'
import { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react'
import { type Event } from 'nostr-tools'
import { type Event as NostrEvent } from 'nostr-tools'
import {
forwardRef,
useCallback,
@ -50,8 +51,8 @@ export type CalendarPrimaryPageProps = { @@ -50,8 +51,8 @@ export type CalendarPrimaryPageProps = {
weekOffset?: number
}
function dedupeCalendarEvents(events: Event[]): Event[] {
const map = new Map<string, Event>()
function dedupeCalendarEvents(events: NostrEvent[]): NostrEvent[] {
const map = new Map<string, NostrEvent>()
for (const e of events) {
const k = replaceableEventDedupeKey(e)
const prev = map.get(k)
@ -85,9 +86,22 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -85,9 +86,22 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
const { navigateToNote } = useSmartNoteNavigation()
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const [panelMode, setPanelMode] = useState<'single' | 'double'>(() => storage.getPanelMode())
const layoutRef = useRef<TPrimaryPageLayoutRef>(null)
const [refreshKey, setRefreshKey] = useState(0)
useEffect(() => {
const onPanelMode = (ev: Event) => {
const d = (ev as CustomEvent<{ mode: 'single' | 'double' }>).detail?.mode
if (d === 'single' || d === 'double') setPanelMode(d)
}
window.addEventListener('panelModeChanged', onPanelMode)
return () => window.removeEventListener('panelModeChanged', onPanelMode)
}, [])
/** Month grid is unreadable in the narrow primary column of double-pane; use the same vertical layout as mobile. */
const useVerticalMonthCalendar = isSmallScreen || panelMode === 'double'
const [activeWeekOffset, setActiveWeekOffset] = useState(weekOffsetProp)
useEffect(() => {
setActiveWeekOffset(weekOffsetProp)
@ -108,7 +122,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -108,7 +122,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
setViewMonth(d.getMonth())
}, [highlightBounds.weekStartMs])
const [rawEvents, setRawEvents] = useState<Event[]>([])
const [rawEvents, setRawEvents] = useState<NostrEvent[]>([])
const [loading, setLoading] = useState(false)
const relayUrls = useMemo(() => {
@ -193,7 +207,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -193,7 +207,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
)
if (cancelled) return
const fromFollowing: Event[] = []
const fromFollowing: NostrEvent[] = []
if (followAuthorsKey) {
const authorList = followAuthorsKey.split('|').filter(Boolean).slice(0, FOLLOWING_CALENDAR_AUTHORS_CAP)
for (let i = 0; i < authorList.length; i += FOLLOWING_CALENDAR_AUTHORS_CHUNK) {
@ -369,7 +383,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -369,7 +383,7 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
<p className="text-center text-sm text-muted-foreground">{t('sidebarCalendarLoading')}</p>
) : null}
{isSmallScreen ? (
{useVerticalMonthCalendar ? (
<div className="flex min-w-0 flex-col gap-2" aria-label={t('calendarPageGridLabel')}>
{Array.from({ length: daysInMonth(viewYear, viewMonth) }, (_, idx) => {
const day = idx + 1
@ -401,17 +415,27 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -401,17 +415,27 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
{list.slice(0, 4).map((ev) => {
const meta = getCalendarEventMeta(ev)
const title = meta.title?.trim() || t('calendarPageUntitledEvent')
const cover = meta.image?.trim()
return (
<li key={replaceableEventDedupeKey(ev)} className="min-w-0">
<button
type="button"
onClick={() => navigateToNote(toNote(ev), ev)}
className={cn(
'w-full truncate rounded-md px-2 py-1.5 text-left text-xs font-medium leading-snug text-primary',
'flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs font-medium leading-snug text-primary',
'hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
)}
>
{title}
{cover ? (
<img
src={cover}
alt=""
loading="lazy"
referrerPolicy="no-referrer"
className="size-8 shrink-0 rounded-md object-cover ring-1 ring-border/50"
/>
) : null}
<span className="min-w-0 truncate">{title}</span>
</button>
</li>
)
@ -471,18 +495,28 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -471,18 +495,28 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
{list.slice(0, 4).map((ev) => {
const meta = getCalendarEventMeta(ev)
const title = meta.title?.trim() || t('calendarPageUntitledEvent')
const cover = meta.image?.trim()
return (
<li key={replaceableEventDedupeKey(ev)} className="min-w-0">
<button
type="button"
onClick={() => navigateToNote(toNote(ev), ev)}
className={cn(
'w-full truncate rounded px-0.5 py-px text-left text-[9px] font-medium leading-tight text-primary underline-offset-2',
'flex w-full min-w-0 items-center gap-0.5 rounded px-0.5 py-px text-left text-[9px] font-medium leading-tight text-primary underline-offset-2',
'hover:bg-muted/80 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring md:text-[10px]'
)}
title={title}
>
{title}
{cover ? (
<img
src={cover}
alt=""
loading="lazy"
referrerPolicy="no-referrer"
className="size-3.5 shrink-0 rounded-sm object-cover ring-1 ring-border/40 md:size-4"
/>
) : null}
<span className="min-w-0 flex-1 truncate">{title}</span>
</button>
</li>
)

16
src/pages/secondary/CalendarDayEventsPage/index.tsx

@ -75,17 +75,29 @@ const CalendarDayEventsPage = forwardRef<TPageRef, { ymd: string; index?: number @@ -75,17 +75,29 @@ const CalendarDayEventsPage = forwardRef<TPageRef, { ymd: string; index?: number
{sorted.map((ev) => {
const meta = getCalendarEventMeta(ev)
const label = meta.title?.trim() || t('calendarPageUntitledEvent')
const cover = meta.image?.trim()
return (
<li key={replaceableEventDedupeKey(ev)}>
<Button
type="button"
variant="ghost"
className={cn(
'h-auto min-h-10 w-full justify-start whitespace-normal px-3 py-2 text-left text-sm font-medium'
'flex h-auto min-h-10 w-full items-center justify-start gap-3 whitespace-normal px-3 py-2 text-left text-sm font-medium'
)}
onClick={() => navigateToNote(toNote(ev), ev)}
>
{label}
{cover ? (
<img
src={cover}
alt=""
loading="lazy"
referrerPolicy="no-referrer"
className="size-9 shrink-0 rounded-md object-cover ring-1 ring-border/50"
/>
) : (
<div className="size-9 shrink-0 rounded-md bg-muted/60 ring-1 ring-border/40" aria-hidden />
)}
<span className="min-w-0 flex-1 leading-snug">{label}</span>
</Button>
</li>
)

Loading…
Cancel
Save