feat: show tagged statuses in tag page
This commit is contained in:
parent
1f91f7add9
commit
3c2665630d
6 changed files with 115 additions and 45 deletions
16
mastodon.py
16
mastodon.py
|
@ -1,4 +1,18 @@
|
|||
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):
|
||||
mastodon = None
|
||||
|
@ -30,5 +44,5 @@ def initialize_client(app):
|
|||
app['password'],
|
||||
to_file = app['secret_file']
|
||||
)
|
||||
|
||||
|
||||
return mastodon
|
|
@ -16,8 +16,10 @@
|
|||
<a href="{{ thread.url }}" title="{{ thread.created_at }}">
|
||||
<relative-time datetime="{{ thread.created_at }}" precision="day">{{ thread.created_at }}</relative-time>
|
||||
</a>
|
||||
{% if not is_tag %}
|
||||
<span>·</span>
|
||||
<a href="{{ url_for('threads.thread', id=parent_id) + '#' + thread['id'] }}">Anchor</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -51,7 +53,7 @@
|
|||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% if is_thread %}
|
||||
{% if is_thread and not is_tag %}
|
||||
<a href="{{ url_for('threads.thread', id=thread.id) }}">Read full thread</a>
|
||||
{% endif %}
|
||||
</article>
|
|
@ -4,7 +4,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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">
|
||||
{% if threads|length == 1 %}
|
||||
<meta name="description" content="{{ threads[0].summary }}" />
|
||||
|
@ -16,7 +16,7 @@
|
|||
<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 }}" />
|
||||
<meta property="og:title" content="{{ app.title }} / {{ tag }}" />
|
||||
|
||||
<script type="module">
|
||||
import TimeAgo from 'https://esm.sh/v135/@github/relative-time-element@4.4.0'
|
||||
|
@ -82,12 +82,6 @@
|
|||
gap: 1em;
|
||||
}
|
||||
|
||||
main.home {
|
||||
& .back {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
main.thread {
|
||||
& .card:not(:last-of-type) .card_avatar::after {
|
||||
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 {
|
||||
border: 2px solid rgba(197, 209, 222, 0.15);
|
||||
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>
|
||||
</head>
|
||||
|
@ -275,10 +300,37 @@
|
|||
<a id="top"></a>
|
||||
<header>
|
||||
{% include "nav.html" %}
|
||||
<h1>{{ app.title }} / #{{ tag }}</h1>
|
||||
<h1>{{ app.title }} / {{ tag }}</h1>
|
||||
<p>{{ app.description }}</p>
|
||||
</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>
|
||||
<p>
|
||||
Copyright ©
|
||||
|
|
|
@ -114,6 +114,7 @@
|
|||
}
|
||||
|
||||
& .pill-label {
|
||||
font-weight: bold;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 0.5em;
|
||||
padding: 0 0.25em;
|
||||
|
@ -314,7 +315,7 @@
|
|||
{% if tags is defined%}
|
||||
<div class="featured-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>
|
||||
{{ tag.name }}
|
||||
</a>
|
||||
|
|
40
threads.py
40
threads.py
|
@ -1,12 +1,10 @@
|
|||
from flask import Blueprint, render_template, current_app
|
||||
import requests
|
||||
from datetime import datetime
|
||||
import markdown
|
||||
import re
|
||||
from .cache import cache
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from . import mastodon
|
||||
from . import mastodon, utils
|
||||
|
||||
threads = Blueprint('threads', __name__, template_folder='templates')
|
||||
|
||||
|
@ -75,7 +73,9 @@ async def home():
|
|||
async def tag(id):
|
||||
attribution = get_attribution()
|
||||
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>')
|
||||
|
@ -85,7 +85,7 @@ def thread(id):
|
|||
attribution = get_attribution()
|
||||
app = get_app_config()
|
||||
status = fetch_thread(id)
|
||||
status['summary'] = clean_html(status['content']).strip()
|
||||
status['summary'] = utils.clean_html(status['content']).strip()
|
||||
if len(status['summary']) > 69:
|
||||
status['summary'] = status['summary'][:69] + '...'
|
||||
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:
|
||||
async with session.get(url, ssl=False) as response:
|
||||
res = await response.json()
|
||||
return clean_status(res)
|
||||
return utils.clean_status(res)
|
||||
except Exception as e:
|
||||
print(f"Unable to get url {url} due to {e.__class__}")
|
||||
return {}
|
||||
|
@ -129,7 +129,7 @@ async def fetch_statuses():
|
|||
|
||||
def fetch_thread(id):
|
||||
status = requests.get(server() + '/api/v1/statuses/' + id ).json()
|
||||
status = clean_status(status)
|
||||
status = utils.clean_status(status)
|
||||
status['descendants'] = get_descendants(server(), 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
|
||||
# - 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:
|
||||
descendants.append(clean_status(reply))
|
||||
descendants.append(utils.clean_status(reply))
|
||||
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
25
utils.py
Normal 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'])
|
Loading…
Reference in a new issue