Skip to content

Commit 3e94c98

Browse files
committed
imeta parsing, kind 20 support
1 parent 9ca2cba commit 3e94c98

File tree

15 files changed

+267
-64
lines changed

15 files changed

+267
-64
lines changed

src/shared/components/embed/media/ImageComponent.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {calculateDimensions, generateBlurhashUrl} from "./mediaUtils"
22
import {useState, MouseEvent, useMemo, memo} from "react"
33
import ProxyImg from "../../ProxyImg"
44
import classNames from "classnames"
5+
import {parseImetaTag} from "@/shared/utils/imetaUtils"
56

67
interface ImageComponentProps {
78
match: string
@@ -20,14 +21,15 @@ const ImageComponent = ({
2021
}: ImageComponentProps) => {
2122
const [hasError, setHasError] = useState(false)
2223

23-
// Extract dimensions from imeta tag if available
24-
const dimensions = imeta?.find((tag) => tag.startsWith("dim "))?.split(" ")[1]
25-
const [originalWidth, originalHeight] = dimensions
26-
? dimensions.split("x").map(Number)
27-
: [null, null]
24+
// Parse imeta data
25+
const imetaData = useMemo(() => {
26+
if (!imeta) return undefined
27+
return parseImetaTag(imeta)
28+
}, [imeta])
2829

29-
// Extract blurhash from imeta tag if available
30-
const blurhash = imeta?.find((tag) => tag.startsWith("blurhash "))?.split(" ")[1]
30+
const originalWidth = imetaData?.width || null
31+
const originalHeight = imetaData?.height || null
32+
const blurhash = imetaData?.blurhash
3133

3234
const calculatedDimensions = calculateDimensions(
3335
originalWidth,
@@ -49,7 +51,7 @@ const ImageComponent = ({
4951
return (
5052
<div
5153
className={classNames("flex justify-center items-center my-2", {
52-
"h-[600px]": limitHeight || !dimensions,
54+
"h-[600px]": limitHeight || !originalWidth || !originalHeight,
5355
})}
5456
>
5557
{hasError ? (
@@ -72,8 +74,9 @@ const ImageComponent = ({
7274
onClick={onClick}
7375
className={classNames("my-2 max-w-full cursor-pointer object-contain", {
7476
"blur-md": blur,
75-
"h-full max-h-[600px]": limitHeight || !dimensions,
76-
"max-h-[90vh] lg:max-h-[600px]": !limitHeight && dimensions,
77+
"h-full max-h-[600px]": limitHeight || !originalWidth || !originalHeight,
78+
"max-h-[90vh] lg:max-h-[600px]":
79+
!limitHeight && originalWidth && originalHeight,
7780
})}
7881
style={{
7982
...calculatedDimensions,

src/shared/components/embed/media/SmallThumbnailComponent.tsx

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import classNames from "classnames"
66
import {EmbedEvent} from "../index"
77
import {generateBlurhashUrl, calculateDimensions, getAllEventMedia} from "./mediaUtils"
88
import MediaModal from "../../media/MediaModal"
9+
import {getImetaDataForUrl} from "@/shared/utils/imetaUtils"
910

1011
interface SmallThumbnailComponentProps {
1112
match: string
@@ -22,30 +23,22 @@ function SmallThumbnailComponent({match, event}: SmallThumbnailComponentProps) {
2223
const [error, setError] = useState(false)
2324
const [showModal, setShowModal] = useState(false)
2425

25-
// Extract imeta tag for this URL
26-
const imetaTag = useMemo(() => {
27-
if (!event?.tags) return undefined
28-
return event.tags.find(
29-
(tag) => tag[0] === "imeta" && tag[1] && tag[1].includes(match)
30-
)
31-
}, [event?.tags, match])
26+
// Extract imeta data for this URL using utility
27+
const imetaData = useMemo(() => {
28+
if (!event) return undefined
29+
return getImetaDataForUrl(event, match)
30+
}, [event, match])
3231

33-
// Extract dimensions from imeta tag if available
34-
const dimensions = imetaTag?.find((tag) => tag.startsWith("dim "))?.split(" ")[1]
35-
const [originalWidth, originalHeight] = dimensions
36-
? dimensions.split("x").map(Number)
37-
: [null, null]
38-
39-
// Extract blurhash from imeta tag if available
40-
const blurhash = imetaTag?.find((tag) => tag.startsWith("blurhash "))?.split(" ")[1]
32+
const originalWidth = imetaData?.width || null
33+
const originalHeight = imetaData?.height || null
34+
const blurhash = imetaData?.blurhash
4135

4236
// Extract alt text from imeta name field and truncate it
4337
const altText = useMemo(() => {
44-
const namePart = imetaTag?.find((tag) => tag.startsWith("name "))
45-
if (!namePart) return "thumbnail"
46-
const name = namePart.substring(5) // Remove "name " prefix
38+
const name = imetaData?.name || imetaData?.alt
39+
if (!name) return "thumbnail"
4740
return name.length > 30 ? name.substring(0, 30) + "..." : name
48-
}, [imetaTag])
41+
}, [imetaData])
4942

5043
// Generate blurhash URL for placeholder (use original dimensions for better aspect ratio)
5144
const blurhashDimensions =

src/shared/components/embed/media/VideoComponent.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {generateProxyUrl} from "../../../utils/imgproxy"
44
import {useSettingsStore} from "@/stores/settings"
55
import classNames from "classnames"
66
import {EmbedEvent} from "../index"
7+
import {parseImetaTag} from "@/shared/utils/imetaUtils"
78

89
interface HlsVideoComponentProps {
910
match: string
@@ -33,14 +34,15 @@ function HlsVideoComponent({
3334
event?.tags.some((t) => t[0] === "content-warning"))
3435
)
3536

36-
// Extract dimensions from imeta tag if available
37-
const dimensions = imeta?.find((tag) => tag.startsWith("dim "))?.split(" ")[1]
38-
const [originalWidth, originalHeight] = dimensions
39-
? dimensions.split("x").map(Number)
40-
: [null, null]
37+
// Parse imeta data
38+
const imetaData = useMemo(() => {
39+
if (!imeta) return undefined
40+
return parseImetaTag(imeta)
41+
}, [imeta])
4142

42-
// Extract blurhash from imeta tag if available
43-
const blurhash = imeta?.find((tag) => tag.startsWith("blurhash "))?.split(" ")[1]
43+
const originalWidth = imetaData?.width || null
44+
const originalHeight = imetaData?.height || null
45+
const blurhash = imetaData?.blurhash
4446

4547
const calculatedDimensions = calculateDimensions(
4648
originalWidth,
@@ -110,7 +112,7 @@ function HlsVideoComponent({
110112
return (
111113
<div
112114
className={classNames("relative w-full justify-center flex object-contain my-2", {
113-
"h-[600px]": limitHeight || !dimensions,
115+
"h-[600px]": limitHeight || !originalWidth || !originalHeight,
114116
})}
115117
>
116118
<video
@@ -128,8 +130,8 @@ function HlsVideoComponent({
128130
ref={videoRef}
129131
className={classNames("max-w-full object-contain", {
130132
"blur-xl": blur,
131-
"h-full max-h-[600px]": limitHeight || !dimensions,
132-
"max-h-[90vh] lg:h-[600px]": !limitHeight && dimensions,
133+
"h-full max-h-[600px]": limitHeight || !originalWidth || !originalHeight,
134+
"max-h-[90vh] lg:h-[600px]": !limitHeight && originalWidth && originalHeight,
133135
})}
134136
style={{
135137
...calculatedDimensions,

src/shared/components/event/FeedItem/FeedItemContent.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import {
1010
KIND_LONG_FORM_CONTENT,
1111
KIND_CLASSIFIED,
1212
KIND_CHANNEL_CREATE,
13+
KIND_PICTURE_FIRST,
1314
} from "@/utils/constants"
1415
import Zapraiser from "../Zapraiser.tsx"
1516
import Highlight from "../Highlight.tsx"
1617
import TextNote from "../TextNote.tsx"
1718
import LongForm from "../LongForm.tsx"
19+
import PictureFirst from "../PictureFirst.tsx"
1820
import {memo} from "react"
1921

2022
type ContentProps = {
@@ -60,6 +62,8 @@ const FeedItemContent = ({event, referredEvent, standalone, truncate}: ContentPr
6062
)
6163
} else if (event.kind === KIND_CHANNEL_CREATE) {
6264
return <ChannelCreation event={event} />
65+
} else if (event.kind === KIND_PICTURE_FIRST) {
66+
return <PictureFirst event={event} truncate={truncate} standalone={standalone} />
6367
} else {
6468
return <TextNote event={event} truncate={truncate} />
6569
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {NDKEvent} from "@nostr-dev-kit/ndk"
2+
import {memo} from "react"
3+
import Carousel from "../embed/media/Carousel"
4+
import HyperText from "../HyperText"
5+
import {extractImetaImages} from "@/shared/utils/imetaUtils"
6+
7+
interface PictureFirstProps {
8+
event: NDKEvent
9+
truncate?: number
10+
standalone?: boolean
11+
}
12+
13+
const PictureFirst = ({event, truncate = 0, standalone}: PictureFirstProps) => {
14+
// Extract images from imeta tags
15+
const images = extractImetaImages(event)
16+
17+
// Get title from title tag if present
18+
const title = event.tagValue("title")
19+
20+
return (
21+
<div className="w-full">
22+
{/* Show images first as carousel if multiple, single if one */}
23+
{images.length > 0 && <Carousel media={images} event={event} />}
24+
25+
{/* Show title if present */}
26+
{title && (
27+
<div className="px-4 mb-2">
28+
<h3 className="text-lg font-semibold">{title}</h3>
29+
</div>
30+
)}
31+
32+
{/* Show content if present */}
33+
{event.content && (
34+
<HyperText event={event} truncate={truncate} expandable={!standalone}>
35+
{event.content}
36+
</HyperText>
37+
)}
38+
</div>
39+
)
40+
}
41+
42+
export default memo(PictureFirst)

src/shared/components/feed/FeedEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ function FeedEditor({
250250
<span className="text-sm text-base-content/70 min-w-[7rem] pt-2">
251251
Event Kinds
252252
</span>
253-
<div className="flex-1">
253+
<div className="flex-1 overflow-hidden">
254254
<EventKindsSelector
255255
selectedKinds={localConfig.filter?.kinds || []}
256256
onKindsChange={(kinds) => {

src/shared/components/feed/ImageGridItem.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import ProxyImg from "../ProxyImg"
1414
import Icon from "../Icons/Icon"
1515
import {LRUCache} from "typescript-lru-cache"
1616
import {ndk} from "@/utils/ndk"
17+
import {KIND_PICTURE_FIRST} from "@/utils/constants"
18+
import {extractImetaImages} from "@/shared/utils/imetaUtils"
1719

1820
interface ImageGridItemProps {
1921
event: NDKEvent | {id: string}
@@ -149,10 +151,17 @@ const ImageGridItem = memo(function ImageGridItem({
149151
const videoMatch = event?.content.match(VIDEO_REGEX)?.[0]
150152

151153
const urls = useMemo(() => {
154+
// For kind 20 events, extract images from imeta tags
155+
if (event?.kind === KIND_PICTURE_FIRST) {
156+
const images = extractImetaImages(event)
157+
return images.map((img) => img.url)
158+
}
159+
160+
// For other events, extract from content
152161
return imageMatch
153162
? imageMatch.trim().split(/\s+/)
154163
: videoMatch?.trim().split(/\s+/) || []
155-
}, [imageMatch, videoMatch])
164+
}, [imageMatch, videoMatch, event?.kind, event?.tags])
156165

157166
// Use smaller sizes for better mobile performance
158167
const isMobile = window.innerWidth <= 767
@@ -226,7 +235,13 @@ const ImageGridItem = memo(function ImageGridItem({
226235
return <div className="aspect-square bg-neutral-300 animate-pulse" />
227236
}
228237

229-
if (event.kind !== 30402 && !hasImageOrVideo(event.content)) {
238+
// Always show kind 20 (picture-first) and kind 30402 (market listings)
239+
// For other kinds, check if content has media
240+
if (
241+
event.kind !== KIND_PICTURE_FIRST &&
242+
event.kind !== 30402 &&
243+
!hasImageOrVideo(event.content)
244+
) {
230245
return null
231246
}
232247

@@ -240,7 +255,11 @@ const ImageGridItem = memo(function ImageGridItem({
240255
}
241256

242257
return urls.map((url, i) => {
243-
const isVideo = !imageMatch
258+
// For kind 20 events, all media are images (not videos)
259+
// For other events, check if it's a video URL
260+
const isVideo =
261+
event?.kind !== KIND_PICTURE_FIRST &&
262+
(videoMatch?.includes(url) || (!imageMatch && videoMatch))
244263
const hasError = loadErrors[i]
245264

246265
const shouldBlur =

src/shared/components/feed/notificationsSubscription.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import {useNotificationsStore} from "@/stores/notifications"
88
import debounce from "lodash/debounce"
99
import {ndk} from "@/utils/ndk"
1010
import {NDKEvent, NDKSubscription} from "@nostr-dev-kit/ndk"
11+
import {
12+
KIND_REACTION,
13+
KIND_REPOST,
14+
KIND_TEXT_NOTE,
15+
KIND_ZAP_RECEIPT,
16+
KIND_HIGHLIGHT,
17+
KIND_PICTURE_FIRST,
18+
} from "@/utils/constants"
1119

1220
let sub: NDKSubscription | undefined
1321

@@ -17,11 +25,12 @@ export const startNotificationsSubscription = debounce((myPubKey?: string) => {
1725
sub?.stop()
1826

1927
const kinds: number[] = [
20-
7, // reactions
21-
6, // reposts
22-
1, // replies
23-
9735, // zap receipts
24-
9802, // highlights
28+
KIND_REACTION,
29+
KIND_REPOST,
30+
KIND_TEXT_NOTE, // replies
31+
KIND_ZAP_RECEIPT,
32+
KIND_HIGHLIGHT,
33+
KIND_PICTURE_FIRST, // when tagged
2534
]
2635

2736
const filters = {
@@ -38,7 +47,7 @@ export const startNotificationsSubscription = debounce((myPubKey?: string) => {
3847
const hideEventsByUnknownUsers = settings.content?.hideEventsByUnknownUsers
3948

4049
sub.on("event", async (event: NDKEvent) => {
41-
if (event.kind !== 9735) {
50+
if (event.kind !== KIND_ZAP_RECEIPT) {
4251
// allow zap notifs from self & unknown users
4352
if (event.pubkey === myPubKey) return
4453
if (hideEventsByUnknownUsers && socialGraph().getFollowDistance(event.pubkey) > 5)
@@ -65,24 +74,27 @@ export const startNotificationsSubscription = debounce((myPubKey?: string) => {
6574
content: event.content,
6675
tags: event.tags,
6776
} as IrisNotification)
68-
const user = event.kind === 9735 ? getZappingUser(event) : event.pubkey
77+
const user = event.kind === KIND_ZAP_RECEIPT ? getZappingUser(event) : event.pubkey
6978
if (!user) {
7079
console.warn("no user for event", event)
7180
return
7281
}
7382
const existing = notification.users.get(user)
7483
if (!existing || existing.time < event.created_at) {
7584
let content: string | undefined = undefined
76-
if (event.kind === 1) {
85+
if (event.kind === KIND_TEXT_NOTE) {
7786
// Text note (reply) content
7887
content = event.content
79-
} else if (event.kind === 7) {
88+
} else if (event.kind === KIND_REACTION) {
8089
// Reaction content (emoji)
8190
content = event.content
82-
} else if (event.kind === 9735) {
91+
} else if (event.kind === KIND_ZAP_RECEIPT) {
8392
// Zap receipt - extract zap amount
8493
const zapAmount = await getZapAmount(event)
8594
content = zapAmount > 0 ? zapAmount.toString() : undefined
95+
} else if (event.kind === KIND_PICTURE_FIRST) {
96+
// Picture-first post content
97+
content = event.content
8698
}
8799

88100
notification.users.set(user, {
@@ -93,9 +105,12 @@ export const startNotificationsSubscription = debounce((myPubKey?: string) => {
93105
if (event.created_at > notification.time) {
94106
notification.time = event.created_at
95107
// Update notification content with the latest reply/reaction
96-
if (event.kind === 1 && event.content) {
108+
if (event.kind === KIND_TEXT_NOTE && event.content) {
97109
// For text notes (replies), update the notification content to show the latest reply
98110
notification.content = event.content
111+
} else if (event.kind === KIND_PICTURE_FIRST && event.content) {
112+
// For picture-first posts, update the notification content
113+
notification.content = event.content
99114
}
100115
}
101116

src/shared/components/ui/EventKindsSelector.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ function EventKindsSelector({
5050
}
5151

5252
return (
53-
<div className={`flex flex-col gap-2 ${className}`}>
54-
<div className="flex gap-2 overflow-x-auto scrollbar-hide pb-2 flex-1">
53+
<div className={`flex flex-col gap-2 w-full ${className}`}>
54+
<div className="flex gap-2 overflow-x-auto scrollbar-hide pb-2 w-full">
5555
{/* Custom event kind input */}
5656
{showCustomInput ? (
5757
<div className="flex items-center gap-1 flex-shrink-0">
@@ -128,6 +128,9 @@ function EventKindsSelector({
128128
</button>
129129
)
130130
})}
131+
132+
{/* Spacer to ensure last item is fully visible */}
133+
<div className="min-w-[1rem] flex-shrink-0" />
131134
</div>
132135
</div>
133136
)

0 commit comments

Comments
 (0)