feat: show tagged statuses in tag page

This commit is contained in:
Ayo Ayco 2025-01-18 19:52:02 +01:00
parent 1f91f7add9
commit 3c2665630d
6 changed files with 115 additions and 45 deletions

View file

@ -1,4 +1,18 @@
from mastodon import Mastodon from mastodon import Mastodon
from . import utils
def get_account_tagged_statuses(app, tag):
mastodon = initialize_client(app)
account = mastodon.me()
print('>>> account id', account.id)
statuses = []
try:
statuses = mastodon.account_statuses(id=account.id, tagged=tag)
except:
print('>>> failed to fetch statuses', tag)
return list(map(lambda x: utils.clean_status(x), statuses))
def initialize_client(app): def initialize_client(app):
mastodon = None mastodon = None

View file

@ -16,8 +16,10 @@
<a href="{{ thread.url }}" title="{{ thread.created_at }}"> <a href="{{ thread.url }}" title="{{ thread.created_at }}">
<relative-time datetime="{{ thread.created_at }}" precision="day">{{ thread.created_at }}</relative-time> <relative-time datetime="{{ thread.created_at }}" precision="day">{{ thread.created_at }}</relative-time>
</a> </a>
{% if not is_tag %}
<span>&middot;</span> <span>&middot;</span>
<a href="{{ url_for('threads.thread', id=parent_id) + '#' + thread['id'] }}">Anchor</a> <a href="{{ url_for('threads.thread', id=parent_id) + '#' + thread['id'] }}">Anchor</a>
{% endif %}
</div> </div>
</div> </div>
@ -51,7 +53,7 @@
{% endif %} {% endif %}
</div> </div>
{% if is_thread %} {% if is_thread and not is_tag %}
<a href="{{ url_for('threads.thread', id=thread.id) }}">Read full thread</a> <a href="{{ url_for('threads.thread', id=thread.id) }}">Read full thread</a>
{% endif %} {% endif %}
</article> </article>

View file

@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ app.title }}</title> <title>{{ app.title }} / {{ tag }}</title>
<meta name="theme-color" content="#3054bf"> <meta name="theme-color" content="#3054bf">
{% if threads|length == 1 %} {% if threads|length == 1 %}
<meta name="description" content="{{ threads[0].summary }}" /> <meta name="description" content="{{ threads[0].summary }}" />
@ -16,7 +16,7 @@
<meta name="author" content="{{ attribution.owner }}" /> <meta name="author" content="{{ attribution.owner }}" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="{{ app.site_name }}" /> <meta property="og:site_name" content="{{ app.site_name }}" />
<meta property="og:title" content="{{ app.title }}" /> <meta property="og:title" content="{{ app.title }} / {{ tag }}" />
<script type="module"> <script type="module">
import TimeAgo from 'https://esm.sh/v135/@github/relative-time-element@4.4.0' import TimeAgo from 'https://esm.sh/v135/@github/relative-time-element@4.4.0'
@ -82,12 +82,6 @@
gap: 1em; gap: 1em;
} }
main.home {
& .back {
display: none
}
}
main.thread { main.thread {
& .card:not(:last-of-type) .card_avatar::after { & .card:not(:last-of-type) .card_avatar::after {
content: " "; content: " ";
@ -99,6 +93,28 @@
} }
} }
.pill {
border-radius: 2em;
background-color: rgba(197, 209, 222, 0.25);
margin: 0 0.5em 0.5em 0;
padding: 1em;
display: inline-block;
font-size: var(--font-size-sm);
color: var(--text-color-dark);
text-decoration: none;
&:hover {
color: var(--color-link);
}
& .pill-label {
background-color: rgba(0, 0, 0, 0.15);
border-radius: 0.5em;
padding: 0 0.25em;
margin-right: 0.25em;
}
}
.card_avatar img { .card_avatar img {
border: 2px solid rgba(197, 209, 222, 0.15); border: 2px solid rgba(197, 209, 222, 0.15);
border-radius: 50%; border-radius: 50%;
@ -267,6 +283,15 @@
} }
} }
} }
.pill {
background-color: rgba(0, 0, 0, 0.15);
color: var(--text-color-light-faded);
&:hover {
color: var(--color-brand-complement);
}
}
} }
</style> </style>
</head> </head>
@ -275,10 +300,37 @@
<a id="top"></a> <a id="top"></a>
<header> <header>
{% include "nav.html" %} {% include "nav.html" %}
<h1>{{ app.title }} / #{{ tag }}</h1> <h1>{{ app.title }} / {{ tag }}</h1>
<p>{{ app.description }}</p> <p>{{ app.description }}</p>
</header> </header>
<main>tag</main> <main class={{ "thread" if threads|length==1 else "home" }}>
{% if tags is defined%}
<div class="featured-tags">
{% for tag in tags %}
<a class="pill" href="{{ tag.url }}">
<span class="pill-label">{{ tag.statuses_count }}</span>
{{ tag.name }}
</a>
{% endfor %}
</div>
{% endif %}
<div class="back">
<a href="{{url_for('threads.home')}}">Back</a>
</div>
{% for thread in threads %}
{% with is_tag=tag!=None, thread=thread, parent_id=thread.id, is_thread=threads|length > 1 %}
{% include "card.html" %}
{% endwith %}
{% if thread.descendants is defined %}
{% for descendant in thread.descendants %}
{% with is_tag=tag!=None, thread=descendant, parent_id=thread.id %}
{% include "card.html" %}
{% endwith %}
{% endfor %}
{% endif %}
{% endfor %}
<a href="#top">Top</a>
</main>
<footer> <footer>
<p> <p>
Copyright &copy; Copyright &copy;

View file

@ -114,6 +114,7 @@
} }
& .pill-label { & .pill-label {
font-weight: bold;
background-color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.15);
border-radius: 0.5em; border-radius: 0.5em;
padding: 0 0.25em; padding: 0 0.25em;
@ -314,7 +315,7 @@
{% if tags is defined%} {% if tags is defined%}
<div class="featured-tags"> <div class="featured-tags">
{% for tag in tags %} {% for tag in tags %}
<a class="pill" href="{{ tag.url }}"> <a class="pill" href="/tag/{{ tag.name }}">
<span class="pill-label">{{ tag.statuses_count }}</span> <span class="pill-label">{{ tag.statuses_count }}</span>
{{ tag.name }} {{ tag.name }}
</a> </a>

View file

@ -1,12 +1,10 @@
from flask import Blueprint, render_template, current_app from flask import Blueprint, render_template, current_app
import requests import requests
from datetime import datetime from datetime import datetime
import markdown
import re
from .cache import cache from .cache import cache
import asyncio import asyncio
import aiohttp import aiohttp
from . import mastodon from . import mastodon, utils
threads = Blueprint('threads', __name__, template_folder='templates') threads = Blueprint('threads', __name__, template_folder='templates')
@ -75,7 +73,9 @@ async def home():
async def tag(id): async def tag(id):
attribution = get_attribution() attribution = get_attribution()
app = get_app_config() app = get_app_config()
return render_template('tag.html', tag=id, app=app, attribution=attribution, render_date=datetime.now()) statuses = mastodon.get_account_tagged_statuses(app, id)
return render_template('tag.html', threads=statuses, tag=id, app=app, attribution=attribution, render_date=datetime.now())
@threads.route('/<path:id>') @threads.route('/<path:id>')
@ -85,7 +85,7 @@ def thread(id):
attribution = get_attribution() attribution = get_attribution()
app = get_app_config() app = get_app_config()
status = fetch_thread(id) status = fetch_thread(id)
status['summary'] = clean_html(status['content']).strip() status['summary'] = utils.clean_html(status['content']).strip()
if len(status['summary']) > 69: if len(status['summary']) > 69:
status['summary'] = status['summary'][:69] + '...' status['summary'] = status['summary'][:69] + '...'
return render_template('threads.html', threads=[status], app=app, attribution=attribution, render_date=datetime.now()) return render_template('threads.html', threads=[status], app=app, attribution=attribution, render_date=datetime.now())
@ -106,7 +106,7 @@ async def get(url, session):
try: try:
async with session.get(url, ssl=False) as response: async with session.get(url, ssl=False) as response:
res = await response.json() res = await response.json()
return clean_status(res) return utils.clean_status(res)
except Exception as e: except Exception as e:
print(f"Unable to get url {url} due to {e.__class__}") print(f"Unable to get url {url} due to {e.__class__}")
return {} return {}
@ -129,7 +129,7 @@ async def fetch_statuses():
def fetch_thread(id): def fetch_thread(id):
status = requests.get(server() + '/api/v1/statuses/' + id ).json() status = requests.get(server() + '/api/v1/statuses/' + id ).json()
status = clean_status(status) status = utils.clean_status(status)
status['descendants'] = get_descendants(server(), status) status['descendants'] = get_descendants(server(), status)
return status return status
@ -141,30 +141,6 @@ def get_descendants(server, status):
# TODO: the following condition will include a reply to a reply of the author # TODO: the following condition will include a reply to a reply of the author
# - edge case: a different author replies in the thread and the author replies then replies again # - edge case: a different author replies in the thread and the author replies then replies again
if reply['account']['id'] == author_id and reply['in_reply_to_account_id'] == author_id: if reply['account']['id'] == author_id and reply['in_reply_to_account_id'] == author_id:
descendants.append(clean_status(reply)) descendants.append(utils.clean_status(reply))
return descendants return descendants
def clean_author(account):
if 'emojis' in account and len(account['emojis']) > 0:
name = account['display_name']
for emoji in account['emojis']:
account['display_name'] = name.replace(":" + emoji['shortcode'] + ":", '<img alt="' + emoji['shortcode'] + ' emoji" class="emoji" src="'+emoji['url']+'" />')
return clean_dict(account, ['avatar', 'display_name', 'id', 'url'])
def clean_status(status):
clean = clean_dict(status, ['id', 'content', 'created_at', 'url', 'media_attachments', 'card'])
clean['account'] = clean_author(status['account'])
clean['content'] = markdown.markdown("<section markdown='block'>"+ clean['content'] +"</section>", extensions=['md_in_html'])
for emoji in status['emojis']:
clean['content'] = clean['content'].replace(":" + emoji['shortcode'] + ":", '<img alt="' + emoji['shortcode'] + ' emoji" class="emoji" src="'+emoji['url']+'" />')
return clean
def clean_dict(dict, keys):
return {k: dict[k] for k in keys}
def clean_html(raw_html):
cleaner = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
return re.sub(cleaner, '', raw_html)

25
utils.py Normal file
View file

@ -0,0 +1,25 @@
import markdown
import re
def clean_status(status):
clean = clean_dict(status, ['id', 'content', 'created_at', 'url', 'media_attachments', 'card'])
clean['account'] = clean_author(status['account'])
clean['content'] = markdown.markdown("<section markdown='block'>"+ clean['content'] +"</section>", extensions=['md_in_html'])
for emoji in status['emojis']:
clean['content'] = clean['content'].replace(":" + emoji['shortcode'] + ":", '<img alt="' + emoji['shortcode'] + ' emoji" class="emoji" src="'+emoji['url']+'" />')
return clean
def clean_dict(dict, keys):
return {k: dict[k] for k in keys}
def clean_html(raw_html):
cleaner = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
return re.sub(cleaner, '', raw_html)
def clean_author(account):
if 'emojis' in account and len(account['emojis']) > 0:
name = account['display_name']
for emoji in account['emojis']:
account['display_name'] = name.replace(":" + emoji['shortcode'] + ":", '<img alt="' + emoji['shortcode'] + ' emoji" class="emoji" src="'+emoji['url']+'" />')
return clean_dict(account, ['avatar', 'display_name', 'id', 'url'])