feat: display featured tags

This commit is contained in:
Ayo Ayco 2025-01-18 16:51:44 +01:00
parent e73ef755fd
commit c7b3d013b4
5 changed files with 394 additions and 39 deletions

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"cSpell.words": ["masto"]
}

View file

@ -4,7 +4,10 @@
"site_name": "Thoughts", "site_name": "Thoughts",
"title": "Thoughts", "title": "Thoughts",
"description": "Hand-picked public posts from my social feed", "description": "Hand-picked public posts from my social feed",
"server" : "https://social.ayco.io" "server": "https://social.ayco.io",
"user": "user@mastodon.social",
"password": "ultraelectromagneticpassword",
"secret_file": "threads-masto-client.secret"
} }
}, },
"ATTRIBUTION": { "ATTRIBUTION": {

300
templates/tag.html Normal file
View file

@ -0,0 +1,300 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ app.title }}</title>
<meta name="theme-color" content="#3054bf">
{% if threads|length == 1 %}
<meta name="description" content="{{ threads[0].summary }}" />
<meta property="og:description" content="{{ threads[0].summary }}" />
{% else %}
<meta name="description" content="{{ app.description }}" />
<meta property="og:description" content="{{ app.description }}" />
{% endif %}
<meta name="author" content="{{ attribution.owner }}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="{{ app.site_name }}" />
<meta property="og:title" content="{{ app.title }}" />
<script type="module">
import TimeAgo from 'https://esm.sh/v135/@github/relative-time-element@4.4.0'
</script>
<link rel="stylesheet" href="{{ url_for('static', filename='variables.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='reset.css') }}" />
<style>
html {
scroll-behavior: smooth;
}
body {
font-family: system-ui, sans-serif;
max-width: 600px;
margin: 0 auto;
color: var(--text-color-dark);
font-size: var(--font-size-base);
display: grid;
padding: 0 1em;
gap: 1em;
a {
color: var(--color-link);
}
small {
font-size: var(--font-size-sm);
}
}
header,
footer {
background: var(--ayo-gradient);
color: var(--text-color-light);
border-radius: 5px;
padding: 1em;
text-wrap: balance;
& a {
color: var(--text-color-light);
}
}
footer {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
main {
& ul.tags {
list-style: none;
padding-left: 0;
& li {
display: inline
}
}
}
main {
display: grid;
gap: 1em;
}
main.home {
& .back {
display: none
}
}
main.thread {
& .card:not(:last-of-type) .card_avatar::after {
content: " ";
display: block;
height: 100%;
border-right: 2px solid rgba(34, 34, 34, 0.15);
width: 26px;
margin-top: -8px;
}
}
.card_avatar img {
border: 2px solid rgba(197, 209, 222, 0.15);
border-radius: 50%;
display: inline;
width: 50px;
}
.card {
grid-template-columns: 55px auto;
display: grid;
gap: 5px;
}
.card_content {
& .invisible {
display: none;
}
& .emoji {
display: inline;
height: calc(1rem + 6px);
margin-bottom: -4px;
}
& .ellipsis::after {
content: '...'
}
& .body {
& code {
font-size: var(--font-size-sm);
background: rgb(245, 242, 240);
padding: 0.25em 0.3em;
border-radius: 5px;
display: inline;
vertical-align: text-bottom;
}
& a:has(.link_card) {
text-decoration: none
}
& .media,
& .link_card {
border: 1px solid rgba(34, 34, 34, 0.15);
border-radius: 5px;
box-shadow: 5px 25px 10px -25px rgba(34, 34, 34, 0.15);
max-width: 100%;
margin: 15px 0 1em;
object-fit: contain;
height: auto;
text-decoration: none;
text-wrap: balance;
}
& .media:hover,
& .link_card:hover {
color: var(--color-link);
text-decoration-color: var(--color-link);
border-color: var(--color-link);
}
& p {
margin-bottom: 1em;
}
}
& .heading {
display: grid;
grid-template-columns: auto auto;
gap: 5px;
height: 20px;
& .author {
font-size: var(--font-size-lg)
}
& .right_menu {
font-size: var(--font-size-sm);
text-align: right;
& a,
& span {
line-height: 36px;
}
& a {
color: var(--text-color-dark);
&:hover {
color: var(--color-link);
}
}
}
}
& .link_card {
color: var(--text-color-dark-faded);
text-decoration: underline;
text-decoration-color: var(--text-color-light-faded);
& strong,
& small {
text-decoration-thickness: 1px;
display: block;
}
padding: 1rem;
}
}
@media (prefers-color-scheme: dark) {
html,
body {
background: var(--bg-darker);
color: var(--text-color-light);
}
main a {
color: var(--color-brand-complement);
}
main.thread {
& .card:not(:last-of-type) .card_avatar::after {
border-right: 2px solid rgba(197, 209, 222, 0.15);
}
}
.card_content {
& .action {
color: var(--color-brand-complement);
}
& .heading .right_menu a {
color: var(--text-color-light);
&:hover {
color: var(--color-brand-complement);
}
}
& .body {
code {
background: rgb(45, 51, 59);
color: rgb(197, 209, 222);
}
& .media,
& .link_card {
border: 1px solid rgba(197, 209, 222, 0.15);
color: var(--text-color-light-faded);
background: var(--bg-dark);
}
& .media:hover,
& .link_card:hover {
color: var(--color-brand-complement);
text-decoration-color: var(--color-brand-complement);
border-color: var(--color-brand-complement);
}
}
}
}
</style>
</head>
<body>
<a id="top"></a>
<header>
{% include "nav.html" %}
<h1>{{ app.title }} / #{{ tag }}</h1>
<p>{{ app.description }}</p>
</header>
<main>tag</main>
<footer>
<p>
Copyright &copy;
{% if attribution.current_year %}
{{ attribution.year}}-{{ attribution.current_year }}
{% else %}
{{ attribution.year}}
{% endif %}
{{ attribution.owner }}
</p>
<p>
Powered by <a href="https://ayco.io/sh/threads">/threads</a>
</p>
<p>Rendered on {{ render_date }} in Europe/Amsterdam</p>
</footer>
</body>
</html>

View file

@ -66,6 +66,17 @@
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
main {
& ul.tags {
list-style: none;
padding-left: 0;
& li {
display: inline
}
}
}
main { main {
display: grid; display: grid;
gap: 1em; gap: 1em;
@ -266,11 +277,24 @@
{% include "nav.html" %} {% include "nav.html" %}
<h1>{{ app.title }}</h1> <h1>{{ app.title }}</h1>
<p>{{ app.description }}</p> <p>{{ app.description }}</p>
</header> </header>
<main class={{ "thread" if threads|length==1 else "home" }}> <main class={{ "thread" if threads|length==1 else "home" }}>
{% if tags is defined%}
<div class="featured-tags">
<strong>Featured tags:</strong>
<ul class="tags">
{% for tag in tags %}
<li><a href="{{ tag.url }}">#{{ tag.name }} ({{ tag.statuses_count }})</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="back"> <div class="back">
<a href="{{url_for('threads.home')}}">Back</a> <a href="{{url_for('threads.home')}}">Back</a>
</div> </div>
<div>
<strong>Featured threads:</strong>
{% for thread in threads %} {% for thread in threads %}
{% with thread=thread, parent_id=thread.id, is_thread=threads|length > 1 %} {% with thread=thread, parent_id=thread.id, is_thread=threads|length > 1 %}
{% include "card.html" %} {% include "card.html" %}
@ -284,6 +308,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<a href="#top">Top</a> <a href="#top">Top</a>
</div>
</main> </main>
<footer> <footer>
<p> <p>

View file

@ -9,6 +9,7 @@ import aiohttp
from mastodon import Mastodon from mastodon import Mastodon
threads = Blueprint('threads', __name__, template_folder='templates') threads = Blueprint('threads', __name__, template_folder='templates')
mastodon = None
# TODO: move following to an app config or sqlite ######### # TODO: move following to an app config or sqlite #########
thread_ids = [ thread_ids = [
@ -60,33 +61,23 @@ async def home():
statuses = await fetch_statuses() statuses = await fetch_statuses()
attribution = get_attribution() attribution = get_attribution()
app = get_app_config() app = get_app_config()
tags = []
Mastodon.create_app( mastodon = await initialize_client(app)
app['site_name'],
api_base_url = app['server'],
to_file = app['secret_file']
)
mastodon = Mastodon(client_id = app['secret_file'],) # List featured hashtags
mastodon.log_in( tags = mastodon.featured_tags()
app['user'], print(tags)
app['password'],
to_file = app['secret_file']
)
response = mastodon.toot('Post from https://ayco.io/threads using mastodon.py!') return render_template('threads.html', threads=statuses, tags=tags, app=app, attribution=attribution, render_date=datetime.now())
print('>>> ' + response.url)
return render_template('threads.html', threads=statuses, app=app, attribution=attribution, render_date=datetime.now())
@threads.route('/new') @threads.route('/tag/<path:id>')
@cache.cached(timeout=300) @cache.cached(timeout=300)
async def new(): async def tag(id):
attribution = get_attribution() attribution = get_attribution()
app = get_app_config() app = get_app_config()
return render_template('new.html', app=app, attribution=attribution, render_date=datetime.now()) return render_template('tag.html', tag=id, app=app, attribution=attribution, render_date=datetime.now())
@threads.route('/<path:id>') @threads.route('/<path:id>')
@ -174,8 +165,41 @@ def clean_status(status):
def clean_dict(dict, keys): def clean_dict(dict, keys):
return {k: dict[k] for k in keys} return {k: dict[k] for k in keys}
CLEANR = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
def clean_html(raw_html): def clean_html(raw_html):
cleantext = re.sub(CLEANR, '', raw_html) cleaner = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
return cleantext return re.sub(cleaner, '', raw_html)
async def initialize_client(app):
global mastodon
secret = None
try:
secret_file = open(app['secret_file'], 'r')
secret = secret_file.read()
except OSError as e:
print('>>> No secret found.')
# todo, check if access_token exist in secret_file
if secret == None:
#...if token does not exist, create app:
Mastodon.create_app(
app['site_name'],
api_base_url = app['server'],
to_file = app['secret_file']
)
mastodon = Mastodon(client_id=app['secret_file'])
print('>>> Persisted new token!')
else:
#... otherwise, reuse
mastodon = Mastodon(access_token=app['secret_file'])
print('>>> Reused persisted token!')
mastodon.log_in(
app['user'],
app['password'],
to_file = app['secret_file']
)
return mastodon