You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
15 KiB
449 lines
15 KiB
{% extends 'layout.html.twig' %} |
|
|
|
{% block title %} |
|
{%- if author and (author.name ?? false) -%} |
|
{{ author.name }} - Nostr Event |
|
{%- else -%} |
|
Nostr Event |
|
{%- endif -%} |
|
{% endblock %} |
|
|
|
{% block ogtags %} |
|
{# Set og:type dynamically based on event kind #} |
|
{% set ogType = 'article' %} |
|
{% if event.kind == 21 or event.kind == 22 %} |
|
{% set ogType = 'video.other' %} |
|
{% elseif event.kind == 20 %} |
|
{% set ogType = 'article' %} {# Could use 'image' but 'article' is more widely supported #} |
|
{% endif %} |
|
<meta property="og:type" content="{{ ogType }}" /> |
|
<meta property="og:title" content="{% if author and (author.name ?? false) %}{{ author.name }} - Nostr Event{% else %}Nostr Event{% endif %}" /> |
|
|
|
{# OG Description - use event content or fallback #} |
|
{% set ogDescription = event.content|default('View this Nostr event')|striptags|slice(0, 200) %} |
|
<meta property="og:description" content="{{ ogDescription }}" /> |
|
|
|
{# URL #} |
|
<meta property="og:url" content="{{ app.request.uri }}" /> |
|
|
|
{# Image - try to extract from event or use author image #} |
|
{% set ogImage = null %} |
|
|
|
{# For picture events (kind 20), try to get image from url tag #} |
|
{% if event.kind == 20 %} |
|
{% for tag in event.tags %} |
|
{% if tag[0] == 'url' and ogImage is null %} |
|
{% set ogImage = tag[1] %} |
|
{% endif %} |
|
{% endfor %} |
|
{% endif %} |
|
|
|
{# For video events (kind 21/22), try to get thumbnail #} |
|
{% if (event.kind == 21 or event.kind == 22) and ogImage is null %} |
|
{% for tag in event.tags %} |
|
{% if tag[0] == 'thumb' and ogImage is null %} |
|
{% set ogImage = tag[1] %} |
|
{% elseif tag[0] == 'image' and ogImage is null %} |
|
{% set ogImage = tag[1] %} |
|
{% endif %} |
|
{% endfor %} |
|
{% endif %} |
|
|
|
{# Fallback to author image if available #} |
|
{% if ogImage is null and author and (author.image ?? false) %} |
|
{% set ogImage = author.image %} |
|
{% endif %} |
|
|
|
{# Use default icon if no image found #} |
|
{% if ogImage is null %} |
|
{% set ogImage = app.request.schemeAndHttpHost ~ asset('icons/apple-touch-icon.png') %} |
|
{% endif %} |
|
|
|
<meta property="og:image" content="{{ ogImage }}" /> |
|
|
|
{# Twitter Card tags #} |
|
<meta name="twitter:card" content="summary_large_image" /> |
|
<meta name="twitter:title" content="{% if author and (author.name ?? false) %}{{ author.name }} - Nostr Event{% else %}Nostr Event{% endif %}" /> |
|
<meta name="twitter:description" content="{{ ogDescription }}" /> |
|
<meta name="twitter:image" content="{{ ogImage }}" /> |
|
|
|
{# Article metadata #} |
|
{% if event.created_at is defined %} |
|
<meta property="article:published_time" content="{{ event.created_at|date('c') }}" /> |
|
{% endif %} |
|
|
|
{% if author and (author.name ?? false) %} |
|
<meta property="article:author" content="{{ author.name }}" /> |
|
{% endif %} |
|
|
|
{# Site name #} |
|
<meta property="og:site_name" content="Decent Newsroom" /> |
|
{% endblock %} |
|
|
|
{% block body %} |
|
{# Title from tags #} |
|
{% set title = 'Untitled' %} |
|
{% for tag in event.tags %} |
|
{% if tag[0] == 'title' %} |
|
{% set title = tag[1] %} |
|
{% endif %} |
|
{% endfor %} |
|
<article class="w-container"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h1 class="card-title">{{ title }}</h1> |
|
</div> |
|
{% if author %} |
|
<div class="byline"> |
|
<span> |
|
{{ 'text.byline'|trans }} <a href="{{ path('author-redirect', {'pubkey': event.pubkey}) }}"> |
|
<twig:Molecules:UserFromNpub :ident="event.pubkey" /> |
|
</a> |
|
</span> |
|
<span> |
|
<small>{{ event.created_at|date('F j, Y') }}</small> |
|
</span> |
|
</div> |
|
{% endif %} |
|
</div> |
|
<div class="card-body"> |
|
{# NIP-68 Picture Event (kind 20) #} |
|
{% if event.kind == 20 %} |
|
{% include 'event/_kind20_picture.html.twig' %} |
|
{# NIP-71 Video Events (kind 21 and 22) #} |
|
{% elseif event.kind == 21 or event.kind == 22 %} |
|
{% include 'event/_kind22_video.html.twig' %} |
|
{% elseif event.kind == 1450 %} |
|
{% include 'event/_kind1450_tabular.html.twig' %} |
|
{% else %} |
|
{# Regular event content for non-picture and non-video events #} |
|
<div class="event-content"> |
|
<twig:Atoms:Content :content="event.content" /> |
|
</div> |
|
{# Collect all imeta images into an array #} |
|
{% set images = [] %} |
|
{% for tag in event.tags %} |
|
{% if tag[0] == 'imeta' %} |
|
{% set imageUrl = null %} |
|
{% set mimeType = null %} |
|
{% set blurhash = null %} |
|
{% set dimensions = null %} |
|
{% set altText = null %} |
|
{% set fallbacks = [] %} |
|
{% set annotatedUsers = [] %} |
|
{# Parse imeta tag parameters #} |
|
{% for i in 1..(tag|length - 1) %} |
|
{% set param = tag[i] %} |
|
{% if param starts with 'url ' %} |
|
{% set imageUrl = param[4:] %} |
|
{% elseif param starts with 'm ' %} |
|
{% set mimeType = param[2:] %} |
|
{% elseif param starts with 'blurhash ' %} |
|
{% set blurhash = param[9:] %} |
|
{% elseif param starts with 'dim ' %} |
|
{% set dimensions = param[4:] %} |
|
{% elseif param starts with 'alt ' %} |
|
{% set altText = param[4:] %} |
|
{% elseif param starts with 'fallback ' %} |
|
{% set fallbacks = fallbacks|merge([param[9:]]) %} |
|
{% elseif param starts with 'annotate-user ' %} |
|
{% set annotatedUsers = annotatedUsers|merge([param[14:]]) %} |
|
{% endif %} |
|
{% endfor %} |
|
{# Alt is also own tag #} |
|
{% for altTag in event.tags %} |
|
{% if altTag[0] == 'alt' %} |
|
{% set altText = altTag[1] %} |
|
{% endif %} |
|
{% endfor %} |
|
{% set images = images|merge([{ |
|
'url': imageUrl, |
|
'mimeType': mimeType, |
|
'blurhash': blurhash, |
|
'dimensions': dimensions, |
|
'altText': altText, |
|
'fallbacks': fallbacks, |
|
'annotatedUsers': annotatedUsers |
|
}]) %} |
|
{% endif %} |
|
{% endfor %} |
|
|
|
{% if images|length > 0 %} |
|
<div class="gallery-view" data-controller="gallery"> |
|
<div class="main-image-wrapper"> |
|
{% set main = images[0] %} |
|
<figure class="media"> |
|
<picture> |
|
<img src="{{ main.url }}" |
|
alt="{{ main.altText|default('Picture') }}" |
|
{% if main.dimensions %}data-dimensions="{{ main.dimensions }}"{% endif %} |
|
{% if main.blurhash %}data-blurhash="{{ main.blurhash }}"{% endif %} |
|
class="picture-image main-image" |
|
data-gallery-target="mainImage" |
|
/> |
|
{% for fallback in main.fallbacks %} |
|
<source srcset="{{ fallback }}" /> |
|
{% endfor %} |
|
</picture> |
|
|
|
{% if images|length > 1 %} |
|
<div class="thumbnails" data-gallery-target="thumbnails"> |
|
{% for img in images %} |
|
<img src="{{ img.url }}" |
|
alt="{{ img.altText|default('Picture') }}" |
|
class="thumbnail{% if loop.first %} selected{% endif %}" |
|
data-gallery-target="thumbnail" |
|
data-action="click->gallery#switch" |
|
data-gallery-index="{{ loop.index0 }}" |
|
{% if img.dimensions %}data-dimensions="{{ img.dimensions }}"{% endif %} |
|
{% if img.blurhash %}data-blurhash="{{ img.blurhash }}"{% endif %} |
|
/> |
|
{% endfor %} |
|
</div> |
|
{% endif %} |
|
</figure> |
|
{# Display annotated users for main image #} |
|
{% if main.annotatedUsers|length > 0 %} |
|
<div class="annotated-users"> |
|
{% for userAnnotation in main.annotatedUsers %} |
|
{% set parts = userAnnotation|split(':') %} |
|
{% if parts|length == 3 %} |
|
<div class="user-tag" data-left="{{ parts[1] }}" data-top="{{ parts[2] }}" style="left: {{ parts[1] }}%; top: {{ parts[2] }}%;"> |
|
<twig:Molecules:UserFromNpub ident="{{ parts[0] }}" /> |
|
</div> |
|
{% endif %} |
|
{% endfor %} |
|
</div> |
|
{% endif %} |
|
</div> |
|
</div> |
|
{% endif %} |
|
|
|
{% endif %} |
|
|
|
</div> |
|
</article> |
|
|
|
<div class="container"> |
|
{% if is_granted('ROLE_ADMIN') and nostrLinks is defined and nostrLinks|length > 0 %} |
|
<div class="nostr-links"> |
|
<h4>Referenced Nostr Links</h4> |
|
<ul class="link-list"> |
|
{% for link in nostrLinks %} |
|
{% if link.type == 'url' %} |
|
<li> |
|
<a href="{{ link.identifier }}">{{ link.identifier }}</a> |
|
</li> |
|
{% elseif link.type == 'npub' %} |
|
<li> |
|
<a href="/p/{{ link.identifier }}">{{ link.identifier }}</a> |
|
<span class="link-type">({{ link.type }})</span> |
|
</li> |
|
{% else %} |
|
<li> |
|
<a href="/e/{{ link.identifier }}">{{ link.identifier }}</a> |
|
<span class="link-type">({{ link.type }})</span> |
|
</li> |
|
{% endif %} |
|
{% endfor %} |
|
</ul> |
|
</div> |
|
{% endif %} |
|
|
|
<div class="event-footer"> |
|
{# Source link from r tag #} |
|
{% set sourceUrl = null %} |
|
{% for tag in event.tags %} |
|
{% if tag[0] == 'r' and tag|length > 2 and tag[2] == 'source' %} |
|
{% set sourceUrl = tag[1] %} |
|
{% endif %} |
|
{% endfor %} |
|
|
|
{% if sourceUrl %} |
|
<div class="picture-source"> |
|
<span class="source-label">Source:</span> |
|
<a href="{{ sourceUrl }}" target="_blank" rel="noopener noreferrer" class="source-link"> |
|
{{ sourceUrl }} |
|
</a> |
|
</div> |
|
{% endif %} |
|
|
|
{% if is_granted('ROLE_ADMIN') %} |
|
<h2>Raw Event Data</h2> |
|
<pre> |
|
{{ event|json_encode(constant('JSON_PRETTY_PRINT')) }} |
|
</pre> |
|
|
|
<div class="event-tags"> |
|
{% if event.tags is defined and event.tags|length > 0 %} |
|
<ul> |
|
{% for tag in event.tags %} |
|
<li> |
|
<strong>{{ tag[0] }}:</strong> {{ tag[1] }} |
|
{% if tag[2] is defined %} |
|
<span>{{ tag[2] }}</span> |
|
{% endif %} |
|
{% if tag[3] is defined %} |
|
<span>{{ tag[3] }}</span> |
|
{% endif %} |
|
</li> |
|
{% endfor %} |
|
</ul> |
|
{% endif %} |
|
</div> |
|
{% endif %} |
|
</div> |
|
</div> |
|
{% endblock %} |
|
|
|
{% block stylesheets %} |
|
{{ parent() }} |
|
<style> |
|
.event-container { |
|
max-width: 800px; |
|
margin: 2rem auto; |
|
background: #fff; |
|
border-radius: 8px; |
|
} |
|
|
|
.event-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
border-bottom: 1px solid #eee; |
|
padding: 1rem; |
|
} |
|
|
|
.event-content { |
|
padding: 1rem; |
|
font-size: 1.1rem; |
|
line-height: 1.6; |
|
} |
|
|
|
/* NIP-68 Picture Event Styles */ |
|
.content-warning { |
|
background-color: #fff3cd; |
|
border: 2px solid #ffc107; |
|
padding: 1rem; |
|
border-radius: 8px; |
|
margin-bottom: 1rem; |
|
text-align: center; |
|
} |
|
|
|
.btn-show-nsfw { |
|
margin-top: 0.5rem; |
|
padding: 0.5rem 1rem; |
|
background-color: #ffc107; |
|
border: none; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
font-weight: bold; |
|
} |
|
|
|
.btn-show-nsfw:hover { |
|
background-color: #e0a800; |
|
} |
|
|
|
.picture-location { |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
margin-bottom: 1rem; |
|
color: #555; |
|
font-size: 0.95rem; |
|
} |
|
|
|
.location-icon { |
|
font-size: 1.2rem; |
|
} |
|
|
|
.geohash { |
|
background-color: #e9ecef; |
|
padding: 0.2rem 0.5rem; |
|
border-radius: 4px; |
|
font-family: monospace; |
|
font-size: 0.85rem; |
|
cursor: help; |
|
} |
|
|
|
.picture-source { |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
margin-bottom: 1rem; |
|
font-size: 0.95rem; |
|
} |
|
|
|
.source-label { |
|
font-weight: 600; |
|
color: #495057; |
|
flex-shrink: 0; |
|
} |
|
|
|
.source-link { |
|
color: #0066cc; |
|
text-decoration: none; |
|
word-break: break-all; |
|
} |
|
|
|
.source-link:hover { |
|
text-decoration: underline; |
|
} |
|
|
|
.nostr-links { |
|
margin: 1.5rem 0; |
|
padding: 1rem; |
|
background-color: #f9f9f9; |
|
border-radius: 4px; |
|
} |
|
|
|
.link-list { |
|
list-style: none; |
|
padding-left: 0; |
|
} |
|
|
|
.link-list li { |
|
word-break: break-all; |
|
} |
|
|
|
.link-type { |
|
color: #6c757d; |
|
font-size: 0.9rem; |
|
margin-left: 0.5rem; |
|
} |
|
|
|
.event-footer { |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: space-between; |
|
padding: 1rem; |
|
border-top: 1px solid #eee; |
|
} |
|
|
|
.event-tags { |
|
flex: 1; |
|
} |
|
|
|
.event-tags ul, .event-references ul { |
|
list-style-type: none; |
|
padding-left: 0; |
|
} |
|
|
|
.event-tags li, .event-references li { |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.error { |
|
color: #dc3545; |
|
padding: 1rem; |
|
text-align: center; |
|
} |
|
|
|
/* Responsive adjustments */ |
|
@media (max-width: 768px) { |
|
.picture-gallery { |
|
grid-template-columns: 1fr; |
|
} |
|
} |
|
</style> |
|
{% endblock %}
|
|
|