From c686cec5b82c06d27df6982fa8afa692446ba45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Sat, 2 Aug 2025 17:36:17 +0200 Subject: [PATCH] Fix previews in comments (cherry picked from commit 915d8883d4773bbf5216d595d7987d0f9dffe866) --- assets/app.js | 1 + .../controllers/nostr_preview_controller.js | 76 ++++++++++------ assets/styles/article.css | 2 +- assets/styles/card.css | 1 + assets/styles/notice.css | 2 +- assets/styles/og.css | 11 +++ assets/styles/theme.css | 24 ++++++ src/Controller/DefaultController.php | 32 +++++++ src/Service/NostrLinkParser.php | 86 ++++++++++++++++--- templates/components/Molecules/Card.html.twig | 8 +- .../Molecules/NostrPreview.html.twig | 14 +++ .../components/Molecules/OgPreview.html.twig | 21 +++++ .../components/Organisms/Comments.html.twig | 2 +- 13 files changed, 234 insertions(+), 46 deletions(-) create mode 100644 assets/styles/og.css create mode 100644 templates/components/Molecules/OgPreview.html.twig diff --git a/assets/app.js b/assets/app.js index 5c8e97e..196585b 100644 --- a/assets/app.js +++ b/assets/app.js @@ -12,6 +12,7 @@ import './styles/layout.css'; import './styles/button.css'; import './styles/card.css'; import './styles/article.css'; +import './styles/og.css'; import './styles/form.css'; import './styles/notice.css'; import './styles/spinner.css'; diff --git a/assets/controllers/nostr_preview_controller.js b/assets/controllers/nostr_preview_controller.js index 09d4348..b69d88b 100644 --- a/assets/controllers/nostr_preview_controller.js +++ b/assets/controllers/nostr_preview_controller.js @@ -16,34 +16,56 @@ export default class extends Controller { async fetchPreview() { try { - // Show loading indicator this.containerTarget.innerHTML = '
Loading preview...
'; - console.log(this.decodedValue); - const data = { - identifier: this.identifierValue, - type: this.typeValue, - decoded: this.decodedValue - }; - fetch("/preview/", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(data) - }) - .then(res => { - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - return res.text(); - }) - .then(data => { - console.log(data); - this.containerTarget.innerHTML = data; - }) - .catch(error => { - console.error("Error:", error); - }); + if (this.typeValue === 'url' && this.fullMatchValue) { + // Fetch OG preview for plain URLs + fetch("/og-preview/", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ url: this.fullMatchValue }) + }) + .then(res => { + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.text(); + }) + .then(data => { + this.containerTarget.innerHTML = data; + }) + .catch(error => { + console.error("Error:", error); + this.containerTarget.innerHTML = `
Unable to load OG preview for ${this.fullMatchValue}
`; + }); + } else { + // Fallback to Nostr preview + const data = { + identifier: this.identifierValue, + type: this.typeValue, + decoded: this.decodedValue + }; + fetch("/preview/", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) + }) + .then(res => { + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + return res.text(); + }) + .then(data => { + this.containerTarget.innerHTML = data; + }) + .catch(error => { + console.error("Error:", error); + }); + } } catch (error) { console.error('Error fetching Nostr preview:', error); this.containerTarget.innerHTML = `
Unable to load preview for ${this.fullMatchValue}
`; diff --git a/assets/styles/article.css b/assets/styles/article.css index 847a89f..856ff1d 100644 --- a/assets/styles/article.css +++ b/assets/styles/article.css @@ -33,7 +33,7 @@ align-items: baseline; margin: 2rem 0; padding-top: 0.5rem; - border-top: 1px solid #8e4585; + border-top: 1px solid var(--color-border); font-size: 1rem; } diff --git a/assets/styles/card.css b/assets/styles/card.css index d3b79f9..18b9b27 100644 --- a/assets/styles/card.css +++ b/assets/styles/card.css @@ -62,6 +62,7 @@ h2.card-title { display: flex; align-items: center; justify-content: space-between; + margin-bottom: 10px; } .card.comment .metadata p { diff --git a/assets/styles/notice.css b/assets/styles/notice.css index a2b1558..7ecc1e3 100644 --- a/assets/styles/notice.css +++ b/assets/styles/notice.css @@ -9,6 +9,6 @@ } .notice.info { - background-color: rgba(95, 115, 85, 0.15); /* Light version of --color-primary */ + background-color: var(--color-bg-light); /* Light version of --color-primary */ color: var(--color-text); /* Use theme text color for better contrast */ } diff --git a/assets/styles/og.css b/assets/styles/og.css new file mode 100644 index 0000000..d5985ef --- /dev/null +++ b/assets/styles/og.css @@ -0,0 +1,11 @@ +.og-preview-card { + max-width: 100%; + padding: 20px; + margin: 10px 0; + background-color: var(--color-bg); +} + +.og-preview-image { + width: 100%; + height: auto; +} diff --git a/assets/styles/theme.css b/assets/styles/theme.css index 9dfa056..6fceda1 100644 --- a/assets/styles/theme.css +++ b/assets/styles/theme.css @@ -1,6 +1,7 @@ @import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&display=swap'); :root { @@ -28,3 +29,26 @@ --color-text-contrast: #f4f1ee; --brand-color: black; } + +[data-theme="space"] { + --color-bg: #141120; /* Deep violet-black */ + --color-bg-light: #1e1a2e; /* Slightly lighter for contrast */ + --color-bg-primary: #251634; /* Rich purple base (darkened brand tone) */ + + --color-text: #f4f0fc; /* Near-white with a lavender hue */ + --color-text-mid: #c9b9eb; /* Soft pastel lavender */ + --color-text-contrast: #0c0712; /* Near-black for contrast */ + + --color-primary: #c095f4; /* Vibrant purple (the new brand) */ + --color-primary-light: #c9b9eb; /* Lighter variant for hovers/effects */ + --color-secondary: #e8bb8d; /* Warm tan/peach for complementary balance */ + --color-secondary-light: #f4d6b6; /* Lighter peach tone */ + --color-secondary-bg: #fbebdb; /* Ultra-light background variant */ + + --color-border: #322c44; /* Subtle purple-gray */ + + --brand-color: rgb(147, 51, 234); + --brand-font: 'Poppins', sans-serif; /* A classic, refined branding font */ + +} + diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index eabcc80..cee4c44 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -10,6 +10,7 @@ use Exception; use FOS\ElasticaBundle\Finder\FinderInterface; use Psr\Cache\InvalidArgumentException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Contracts\Cache\CacheInterface; @@ -160,4 +161,35 @@ class DefaultController extends AbstractController 'index' => $catIndex ]); } + + /** + * OG Preview endpoint for URLs + */ + #[Route('/og-preview/', name: 'og_preview', methods: ['POST'])] + public function ogPreview(RequestStack $requestStack): Response + { + $request = $requestStack->getCurrentRequest(); + $data = json_decode($request->getContent(), true); + $url = $data['url'] ?? null; + if (!$url) { + return new Response('
No URL provided.
', 400); + } + try { + $embed = new \Embed\Embed(); + $info = $embed->get($url); + if (!$info) { + throw new \Exception('No OG data found'); + } + return $this->render('components/Molecules/OgPreview.html.twig', [ + 'og' => [ + 'title' => $info->title, + 'description' => $info->description, + 'image' => $info->image, + 'url' => $url + ] + ]); + } catch (\Exception $e) { + return new Response('
Unable to load OG preview for ' . htmlspecialchars($url) . '
', 200); + } + } } diff --git a/src/Service/NostrLinkParser.php b/src/Service/NostrLinkParser.php index 1f82bd6..62e3974 100644 --- a/src/Service/NostrLinkParser.php +++ b/src/Service/NostrLinkParser.php @@ -7,8 +7,9 @@ use Psr\Log\LoggerInterface; readonly class NostrLinkParser { - private const string NOSTR_LINK_PATTERN = '/(?:nostr:)?(nevent1[a-z0-9]+|naddr1[a-z0-9]+|nprofile1[a-z0-9]+|note1[a-z0-9]+|npub1[a-z0-9]+)/'; + private const string NOSTR_LINK_PATTERN = '/(?:nostr:)(nevent1[a-z0-9]+|naddr1[a-z0-9]+|nprofile1[a-z0-9]+|note1[a-z0-9]+|npub1[a-z0-9]+)/'; + private const URL_PATTERN = '/https?:\/\/[\w\-\.\?\,\'\/\\\+&%@\?\$#_=:\(\)~;]+/i'; public function __construct( private LoggerInterface $logger @@ -23,36 +24,95 @@ readonly class NostrLinkParser public function parseLinks(string $content): array { $links = []; + $links = array_merge( + $this->parseUrlsWithNostrIds($content), + $this->parseBareNostrIdentifiers($content) + ); + // Sort by position to maintain the original order in the text + usort($links, fn($a, $b) => $a['position'] <=> $b['position']); + return $links; + } + + private function parseUrlsWithNostrIds(string $content): array + { + $links = []; + if (preg_match_all(self::URL_PATTERN, $content, $urlMatches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + foreach ($urlMatches as $urlMatch) { + $url = $urlMatch[0][0]; + $position = $urlMatch[0][1]; + $nostrId = null; + $nostrType = null; + $nostrData = null; + if (preg_match(self::NOSTR_LINK_PATTERN, $url, $nostrMatch)) { + $nostrId = $nostrMatch[1]; + try { + $decoded = new Bech32($nostrId); + $nostrType = $decoded->type; + $nostrData = $decoded->data; + } catch (\Exception $e) { + $this->logger->info('Failed to decode Nostr identifier in URL', [ + 'identifier' => $nostrId, + 'error' => $e->getMessage() + ]); + } + } + $links[] = [ + 'type' => $nostrType ?? 'url', + 'identifier' => $nostrId, + 'full_match' => $url, + 'position' => $position, + 'data' => $nostrData, + 'is_url' => true + ]; + } + } + return $links; + } + + private function parseBareNostrIdentifiers(string $content): array + { + $links = []; + - // Improved regular expression to match all nostr: links - // This will find all occurrences including multiple links in the same text if (preg_match_all(self::NOSTR_LINK_PATTERN, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + + // If match starts with nostr:, continue otherwise check if part of URL + if (!(str_starts_with($matches[0][0][0], 'nostr:'))) { + // Check if the match is part of a URL, as path or query parameter + $urlPattern = '/https?:\/\/[\w\-.?,\'\/+&%$#@_=:()~;]+/i'; + foreach ($matches as $key => $match) { + $position = $match[0][1]; + // Check if the match is preceded by a URL + $precedingContent = substr($content, 0, $position); + + if (preg_match($urlPattern, $precedingContent)) { + // If the match is preceded by a URL, skip it + unset($matches[$key]); + } + } + } + foreach ($matches as $match) { - $fullMatch = $match[0][0]; $identifier = $match[1][0]; - $position = $match[0][1]; // Position in the text - + $position = $match[0][1]; + // This check will be handled in parseLinks by sorting and merging try { $decoded = new Bech32($identifier); $links[] = [ 'type' => $decoded->type, 'identifier' => $identifier, - 'full_match' => $fullMatch, + 'full_match' => $match[0][0], 'position' => $position, - 'data' => $decoded->data + 'data' => $decoded->data, + 'is_url' => false ]; } catch (\Exception $e) { - // If decoding fails, skip this identifier $this->logger->info('Failed to decode Nostr identifier', [ 'identifier' => $identifier, 'error' => $e->getMessage() ]); - continue; } } - - // Sort by position to maintain the original order in the text - usort($links, fn($a, $b) => $a['position'] <=> $b['position']); } return $links; diff --git a/templates/components/Molecules/Card.html.twig b/templates/components/Molecules/Card.html.twig index 65d36a7..2f8306b 100644 --- a/templates/components/Molecules/Card.html.twig +++ b/templates/components/Molecules/Card.html.twig @@ -17,9 +17,11 @@

{{ article.title }}

-

- {{ article.summary }} -

+ {% if article.summary %} +

+ {{ article.summary }} +

+ {% endif %}
diff --git a/templates/components/Molecules/NostrPreview.html.twig b/templates/components/Molecules/NostrPreview.html.twig index 12ca3fd..bf49ef3 100644 --- a/templates/components/Molecules/NostrPreview.html.twig +++ b/templates/components/Molecules/NostrPreview.html.twig @@ -9,5 +9,19 @@
Loading preview... + {% if preview.full_match is defined and preview.full_match %} + + {% endif %} + {% if preview.data is not null %} +
+ {# Example: show kind if available #} + {% if preview.data.kind is defined %} + Kind: {{ preview.data.kind }} + {% endif %} + {# Add more event details here as needed #} +
+ {% endif %} diff --git a/templates/components/Molecules/OgPreview.html.twig b/templates/components/Molecules/OgPreview.html.twig new file mode 100644 index 0000000..72d2ed6 --- /dev/null +++ b/templates/components/Molecules/OgPreview.html.twig @@ -0,0 +1,21 @@ +
+ {% if og.image %} +
+ + OG image for {{ og.title }} + +
+ {% endif %} +
+ {% if og.title %} + {{ og.url }} + {% endif %} + + {% if og.description %} +
{{ og.description }}
+ {% endif %} +
+
+ diff --git a/templates/components/Organisms/Comments.html.twig b/templates/components/Organisms/Comments.html.twig index 4c6bf28..c4b215d 100644 --- a/templates/components/Organisms/Comments.html.twig +++ b/templates/components/Organisms/Comments.html.twig @@ -27,7 +27,7 @@