Description
On Nostr timelines, some posts with an image or video URL don't render any media at all — the URL just shows up as a plain text link. This happens specifically when the publisher puts the media URL in the middle of a line or paragraph, rather than as the last token on its own trailing line.
I'm a Flare user browsing mixed Nostr timelines daily and ran into this often enough to dig into why.
Steps to reproduce
Post text like this (no imeta or r tags, just inline content):
check this out https://example.com/photo.jpg it's great
Expected: the image renders as a media attachment on the post.
Actual: the URL renders as a plain clickable text link; no image card appears.
Root cause
In social/nostr/src/commonMain/kotlin/dev/dimension/flare/data/network/nostr/NostrRichTextParser.kt, the function that pulls untagged media URLs out of the post body (extractTrailingMediaUrls) only checks the last whitespace-separated token of the last non-blank line. Any media URL that appears earlier in the text — mid-sentence, or on a line that isn't the final one — is never extracted, so it's never added to the post's image list. It still gets treated as a normal link elsewhere in the parser, which is why the URL is visible as text but the image itself is missing.
Suggested fix
I've written a fix that generalizes the extraction to scan every token on every line, not just the trailing one, and removes matched tokens from the display text the same way the original trailing-only version did. It's a net simplification — two now-unused helper functions (lastWhitespaceSeparatedTokenOrNull, removeTrailingToken) go away since the new version doesn't need them.
I also added two new unit tests alongside the existing ones in NostrRichTextParserTest.kt:
- a media URL embedded mid-sentence
- multiple media URLs spread across different lines
All existing tests should still pass under the new logic — I traced the logic by hand against each one and they hold up, but I haven't been able to run ./gradlew test or ktlintFormat in my own setup. I'd rather flag that plainly than claim it's verified: if a maintainer or another contributor can run the test suite and lint against this patch, that'd be a big help before it's considered mergeable.
Patch attached: [nostr-mid-text-media-url-fix.patch](https://www.swisstransfer.com/d/25b5dd3c-c760-40ca-a55d-21047a3d71bf)
Happy to open this as a PR directly if that's preferred, though I should mention I'm not set up to run the local build/test suite myself right now — I can share the diff and reasoning, but would rely on a maintainer or another contributor to actually run and verify it. Let me know if that works or if you'd rather I hold off on a PR until I can test properly.
One open question for maintainers: this fix will now also pull out media-looking URLs (e.g. ending in .jpg) from anywhere in the text, including cases where a publisher intended it as a plain link rather than an attachment. I don't see an easy way to distinguish "meant as embedded media" from "meant as a link that happens to point to an image" without an explicit tag (like imeta) — curious if this tradeoff seems right to you, or if there's a pattern in the codebase I'm missing for that distinction.
Description
On Nostr timelines, some posts with an image or video URL don't render any media at all — the URL just shows up as a plain text link. This happens specifically when the publisher puts the media URL in the middle of a line or paragraph, rather than as the last token on its own trailing line.
I'm a Flare user browsing mixed Nostr timelines daily and ran into this often enough to dig into why.
Steps to reproduce
Post text like this (no
imetaorrtags, just inline content):Expected: the image renders as a media attachment on the post.
Actual: the URL renders as a plain clickable text link; no image card appears.
Root cause
In
social/nostr/src/commonMain/kotlin/dev/dimension/flare/data/network/nostr/NostrRichTextParser.kt, the function that pulls untagged media URLs out of the post body (extractTrailingMediaUrls) only checks the last whitespace-separated token of the last non-blank line. Any media URL that appears earlier in the text — mid-sentence, or on a line that isn't the final one — is never extracted, so it's never added to the post's image list. It still gets treated as a normal link elsewhere in the parser, which is why the URL is visible as text but the image itself is missing.Suggested fix
I've written a fix that generalizes the extraction to scan every token on every line, not just the trailing one, and removes matched tokens from the display text the same way the original trailing-only version did. It's a net simplification — two now-unused helper functions (
lastWhitespaceSeparatedTokenOrNull,removeTrailingToken) go away since the new version doesn't need them.I also added two new unit tests alongside the existing ones in
NostrRichTextParserTest.kt:All existing tests should still pass under the new logic — I traced the logic by hand against each one and they hold up, but I haven't been able to run
./gradlew testorktlintFormatin my own setup. I'd rather flag that plainly than claim it's verified: if a maintainer or another contributor can run the test suite and lint against this patch, that'd be a big help before it's considered mergeable.Patch attached:
[nostr-mid-text-media-url-fix.patch](https://www.swisstransfer.com/d/25b5dd3c-c760-40ca-a55d-21047a3d71bf)Happy to open this as a PR directly if that's preferred, though I should mention I'm not set up to run the local build/test suite myself right now — I can share the diff and reasoning, but would rely on a maintainer or another contributor to actually run and verify it. Let me know if that works or if you'd rather I hold off on a PR until I can test properly.
One open question for maintainers: this fix will now also pull out media-looking URLs (e.g. ending in
.jpg) from anywhere in the text, including cases where a publisher intended it as a plain link rather than an attachment. I don't see an easy way to distinguish "meant as embedded media" from "meant as a link that happens to point to an image" without an explicit tag (likeimeta) — curious if this tradeoff seems right to you, or if there's a pattern in the codebase I'm missing for that distinction.