Tweak design and AP tag supports

main
Thomas Sileo 2022-07-01 19:35:34 +02:00
parent 9a4643fa3e
commit d164d6d2dd
8 changed files with 189 additions and 66 deletions

View File

@ -551,15 +551,35 @@ async def tag_by_name(
db_session: AsyncSession = Depends(get_db_session), db_session: AsyncSession = Depends(get_db_session),
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse | templates.TemplateResponse: ) -> ActivityPubResponse | templates.TemplateResponse:
where = [
models.TaggedOutboxObject.tag == tag,
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False),
]
tagged_count = await db_session.scalar(
select(func.count(models.OutboxObject.id))
.join(models.TaggedOutboxObject)
.where(*where)
)
if not tagged_count:
raise HTTPException(status_code=404)
outbox_objects = await db_session.execute(
select(models.OutboxObject.ap_id)
.join(models.TaggedOutboxObject)
.where(*where)
.order_by(models.OutboxObject.ap_published_at.desc())
.limit(20)
)
# TODO(ts): implement HTML version # TODO(ts): implement HTML version
# if is_activitypub_requested(request): # if is_activitypub_requested(request):
return ActivityPubResponse( return ActivityPubResponse(
{ {
"@context": ap.AS_CTX, # XXX: extended ctx? "@context": ap.AS_CTX,
"id": BASE_URL + f"/t/{tag}", "id": BASE_URL + f"/t/{tag}",
"type": "OrderedCollection", "type": "OrderedCollection",
"totalItems": 0, "totalItems": tagged_count,
"orderedItems": [], "orderedItems": [outbox_object.ap_id for outbox_object in outbox_objects],
} }
) )
@ -876,7 +896,8 @@ async def robots_file():
return """User-agent: * return """User-agent: *
Disallow: /followers Disallow: /followers
Disallow: /following Disallow: /following
Disallow: /admin""" Disallow: /admin
Disallow: /remote_follow"""
async def _get_outbox_for_feed(db_session: AsyncSession) -> list[models.OutboxObject]: async def _get_outbox_for_feed(db_session: AsyncSession) -> list[models.OutboxObject]:

View File

@ -1,20 +1,92 @@
$font-stack: Helvetica, sans-serif;
$background: #002B36; // solarized background color
// font-family: Inconsolata, monospace;
$primary-color: #e14eea;
$secondary-color: #32cd32;
$muted-color: #586e75; // solarized comment text
// #93a1a1; solarized body text
body { body {
font-family: $font-stack;
font-size: 20px;
line-height: 32px;
background: $background;
color: #ccc;
margin: 0; margin: 0;
padding: 0; padding: 0;
display: flex; display: flex;
min-height: 100vh; min-height: 100vh;
flex-direction: column; flex-direction: column;
} }
a {
text-decoration: none;
}
.activity-main {
a {
color: #ddd;
}
}
.activity-wrap {
}
code, pre {
color: #859900; // #cb4b16; // #268bd2; // #2aa198;
font-family: 'Inconsolata, monospace';
}
header {
.title {
font-size: 1.3em;
text-decoration: none;
.handle {
font-size: 0.85em;
color: #93a1a1;
}
}
.counter {
color: #93a1a1;
}
}
.purple {
color: #e14eea;
}
a {
color: #e14eea;
}
.green, a:hover {
color: #32cd32;
}
#main { #main {
flex: 1; flex: 1;
} }
main { main {
max-width: 800px; width: 100%;
margin: 20px auto; max-width: 960px;
margin: 30px auto;
} }
footer { footer {
max-width: 800px; width: 100%;
max-width: 960px;
margin: 20px auto; margin: 20px auto;
color: #93a1a1;
}
.actor-box {
display: flex;
column-gap: 20px;
margin:20px 0 10px 0;
.icon-box {
flex: 0 0 50px;
}
.actor-handle {
font-size: 0.85em;
line-height: 1em;
color: #93a1a1;
}
.actor-icon {
margin-top: 5px;
max-width: 50px;
}
} }
#notifications, #followers, #following { #notifications, #followers, #following {
ul { ul {
@ -26,47 +98,47 @@ footer {
display: inline-block; display: inline-block;
} }
} }
.actor-box {
a { #admin {
text-decoration: none; }
.activity-bar {
input[type=submit], button {
font-size: 20px;
line-height: 32px;
font-family: "Inconsolata, monospace";
background: none!important;
border: none;
padding: 0!important;
cursor: pointer;
color: #e14eea;
}
input[type=submit]:hover, button:hover {
color: #32cd32;
} }
} }
#admin {
.navbar {
display: grid;
grid-template-rows: auto;
grid-template-columns: 1fr;
grid-auto-flow: dense;
justify-items: stretch;
align-items: stretch;
column-gap: 20px;
}
.logo {
grid-column:-3;
padding: 5px;
}
.menus {
display: flex;
flex-direction: row;
justify-content: start;
grid-column: 1;
}
.menus * {
padding: 5px;
}
}
nav.flexbox { nav.flexbox {
display: flex;
justify-content: space-between; input[type=submit] {
align-items: center; font-size: 20px;
line-height: 32px;
font-family: "Inconsolata, monospace";
background: none!important;
border: none;
padding: 0!important;
cursor: pointer;
color: #e14eea;
}
input[type=submit]:hover {
color: #32cd32;
}
ul { ul {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
@ -81,12 +153,13 @@ nav.flexbox {
margin-right: 0px; margin-right: 0px;
} }
} }
} a {
#admin {
a.active {
font-weight: bold;
text-decoration: none; text-decoration: none;
} }
a.active {
color: #32cd32;
font-weight: bold;
}
} }
@ -94,12 +167,11 @@ nav.flexbox {
margin: 0 auto; margin: 0 auto;
padding: 30px 0; padding: 30px 0;
.actor-icon { .actor-icon {
width:48px; width: 50px;
margin-right: 15px; margin-right: 15px;
img { margin-top: 5px;
max-width: 48px;
}
} }
.activity-content { .activity-content {
display: flex; display: flex;
align-items:flex-start; align-items:flex-start;
@ -112,6 +184,9 @@ nav.flexbox {
font-weight:normal; font-weight:normal;
margin-left: 5px; margin-left: 5px;
} }
.actor-handle {
color: #93a1a1;
}
.activity-date { float:right; } .activity-date { float:right; }
} }
} }

View File

@ -18,7 +18,7 @@
</select> </select>
</p> </p>
{% for emoji in emojis %} {% for emoji in emojis %}
<span class="ji">{{ emoji | emojify | safe }}</span> <span class="ji">{{ emoji | emojify(True) | safe }}</span>
{% endfor %} {% endfor %}
{% for emoji in custom_emojis %} {% for emoji in custom_emojis %}
<span class="ji"><img src="{{ emoji.icon.url }}" alt="{{ emoji.name }}" title="{{ emoji.name }}" class="custom-emoji"></span> <span class="ji"><img src="{{ emoji.icon.url }}" alt="{{ emoji.name }}" title="{{ emoji.name }}" class="custom-emoji"></span>

View File

@ -3,8 +3,8 @@
<div class="h-card p-author"> <div class="h-card p-author">
<data class="u-photo" value="{{ local_actor.icon_url }}"></data> <data class="u-photo" value="{{ local_actor.icon_url }}"></data>
<a href="{{ local_actor.url }}" class="u-url u-uid no-hover title"> <a href="{{ local_actor.url }}" class="u-url u-uid no-hover title">
<span style="font-size:1.1em;">{{ local_actor.name }}</span> <span class="name">{{ local_actor.name }}</span>
<span style="font-size:0.85em;" class="p-name subtitle-username">{{ local_actor.handle }}</span> <span class="p-name handle">{{ local_actor.handle }}</span>
</a> </a>
<div class="p-note summary"> <div class="p-note summary">
@ -22,8 +22,8 @@
<nav class="flexbox"> <nav class="flexbox">
<ul> <ul>
<li>{{ header_link("index", "Notes") }}</li> <li>{{ header_link("index", "Notes") }}</li>
<li>{{ header_link("followers", "Followers") }} <span>{{ followers_count }}</span></li> <li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li>
<li>{{ header_link("following", "Following") }} <span>{{ following_count }}</span></li> <li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li> <li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
</ul> </ul>
</nav> </nav>

View File

@ -2,6 +2,7 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block head %} {% block head %}
{% if outbox_object %}
<link rel="alternate" href="{{ request.url }}" type="application/activity+json"> <link rel="alternate" href="{{ request.url }}" type="application/activity+json">
<meta name="description" content="{{ outbox_object.content | html2text | trim | truncate(50) }}"> <meta name="description" content="{{ outbox_object.content | html2text | trim | truncate(50) }}">
<meta content="article" property="og:type" /> <meta content="article" property="og:type" />
@ -11,6 +12,7 @@
<meta content="{{ outbox_object.content | html2text | trim | truncate(50) }}" property="og:description" /> <meta content="{{ outbox_object.content | html2text | trim | truncate(50) }}" property="og:description" />
<meta content="{{ local_actor.icon_url }}" property="og:image" /> <meta content="{{ local_actor.icon_url }}" property="og:image" />
<meta content="summary" property="twitter:card" /> <meta content="summary" property="twitter:card" />
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -136,13 +136,13 @@
{% macro display_actor(actor, actors_metadata) %} {% macro display_actor(actor, actors_metadata) %}
{% set metadata = actors_metadata.get(actor.ap_id) %} {% set metadata = actors_metadata.get(actor.ap_id) %}
<div style="display: flex;column-gap: 20px;margin:20px 0 10px 0;" class="actor-box h-card p-author"> <div class="actor-box h-card p-author">
<div style="flex: 0 0 48px;"> <div class="icon-box">
<img src="{{ actor.resized_icon_url }}" style="max-width:45px;"> <img src="{{ actor.resized_icon_url }}" class="actor-icon">
</div> </div>
<a href="{{ actor.url }}" class="u-url" style=""> <a href="{{ actor.url }}" class="u-url" style="">
<div><strong>{{ actor.display_name | clean_html(actor) | safe }}</strong></div> <div><strong>{{ actor.display_name | clean_html(actor) | safe }}</strong></div>
<div class="p-name">{{ actor.handle }}</div> <div class="actor-handle p-name">{{ actor.handle }}</div>
</a> </a>
</div> </div>
{% if is_admin and metadata %} {% if is_admin and metadata %}
@ -171,14 +171,13 @@
{% macro display_og_meta(object) %} {% macro display_og_meta(object) %}
{% if object.og_meta %} {% if object.og_meta %}
{% for og_meta in object.og_meta %} {% for og_meta in object.og_meta %}
<div style="display:flex;column-gap: 20px;margin:20px 0;"> <div class="activity-og-meta" style="display:flex;column-gap: 20px;margin:20px 0;">
{% if og_meta.image %} {% if og_meta.image %}
<div> <div>
<img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;"> <img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;">
</div> </div>
<div> <div>
<a href="{{ og_meta.url }}">{{ og_meta.title }}</a> <a href="{{ og_meta.url }}">{{ og_meta.title }}</a>
{% if og_meta.description %}<p>{{ og_meta.description }}</p>{% endif %}
<small style="display:block;">{{ og_meta.site_name }}</small> <small style="display:block;">{{ og_meta.site_name }}</small>
</div> </div>
{% endif %} {% endif %}
@ -279,7 +278,7 @@
<img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon"> <img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon">
<div class="activity-header"> <div class="activity-header">
<strong>{{ object.actor.display_name }}</strong> <strong>{{ object.actor.display_name }}</strong>
<a href="{{ object.actor.url}}" class="p-author h-card">{{ object.actor.handle }}</a> <a href="{{ object.actor.url}}" class="actor-handle p-author h-card">{{ object.actor.handle }}</a>
<span class="activity-date" title="{{ object.ap_published_at.isoformat() }}"> <span class="activity-date" title="{{ object.ap_published_at.isoformat() }}">
{{ object.visibility.value }} {{ object.visibility.value }}
<a href="{{ object.url }}" class="u-url u-uid"><time class="dt-published" datetime="{{ object.ap_published_at }}">{{ object.ap_published_at | timeago }}</time></a> <a href="{{ object.url }}" class="u-url u-uid"><time class="dt-published" datetime="{{ object.ap_published_at }}">{{ object.ap_published_at | timeago }}</time></a>

View File

@ -16,6 +16,19 @@ from tests import factories
from tests.utils import generate_admin_session_cookies from tests.utils import generate_admin_session_cookies
def test_outbox__no_activities(
db: Session,
client: TestClient,
) -> None:
response = client.get("/outbox", headers={"Accept": ap.AP_CONTENT_TYPE})
assert response.status_code == 200
json_response = response.json()
assert json_response["totalItems"] == 0
assert json_response["orderedItems"] == []
def test_send_follow_request( def test_send_follow_request(
db: Session, db: Session,
client: TestClient, client: TestClient,

13
tests/test_tags.py 100644
View File

@ -0,0 +1,13 @@
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app import activitypub as ap
def test_tags__no_tags(
db: Session,
client: TestClient,
) -> None:
response = client.get("/t/nope", headers={"Accept": ap.AP_CONTENT_TYPE})
assert response.status_code == 404