mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	Tweak design and AP tag supports
This commit is contained in:
		
							
								
								
									
										29
									
								
								app/main.py
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								app/main.py
									
									
									
									
									
								
							| @@ -551,15 +551,35 @@ async def tag_by_name( | ||||
|     db_session: AsyncSession = Depends(get_db_session), | ||||
|     _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), | ||||
| ) -> 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 | ||||
|     # if is_activitypub_requested(request): | ||||
|     return ActivityPubResponse( | ||||
|         { | ||||
|             "@context": ap.AS_CTX,  # XXX: extended ctx? | ||||
|             "@context": ap.AS_CTX, | ||||
|             "id": BASE_URL + f"/t/{tag}", | ||||
|             "type": "OrderedCollection", | ||||
|             "totalItems": 0, | ||||
|             "orderedItems": [], | ||||
|             "totalItems": tagged_count, | ||||
|             "orderedItems": [outbox_object.ap_id for outbox_object in outbox_objects], | ||||
|         } | ||||
|     ) | ||||
|  | ||||
| @@ -876,7 +896,8 @@ async def robots_file(): | ||||
|     return """User-agent: * | ||||
| Disallow: /followers | ||||
| Disallow: /following | ||||
| Disallow: /admin""" | ||||
| Disallow: /admin | ||||
| Disallow: /remote_follow""" | ||||
|  | ||||
|  | ||||
| async def _get_outbox_for_feed(db_session: AsyncSession) -> list[models.OutboxObject]: | ||||
|   | ||||
| @@ -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 { | ||||
|     margin: 0; | ||||
|     padding: 0; | ||||
|     display: flex; | ||||
|     min-height: 100vh; | ||||
|     flex-direction: column; | ||||
|   font-family: $font-stack; | ||||
|   font-size: 20px; | ||||
|   line-height: 32px; | ||||
|   background: $background; | ||||
|   color: #ccc; | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   display: flex; | ||||
|   min-height: 100vh; | ||||
|   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 { | ||||
|     flex: 1;  | ||||
| } | ||||
| main { | ||||
|     max-width: 800px; | ||||
|     margin: 20px auto; | ||||
|     width: 100%; | ||||
|     max-width: 960px; | ||||
|     margin: 30px auto; | ||||
| } | ||||
| footer { | ||||
|     max-width: 800px; | ||||
|     width: 100%; | ||||
|     max-width: 960px; | ||||
|     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 { | ||||
|     ul { | ||||
| @@ -26,47 +98,47 @@ footer { | ||||
|         display: inline-block; | ||||
|     } | ||||
| } | ||||
| .actor-box { | ||||
|     a { | ||||
|         text-decoration: none; | ||||
|  | ||||
| #admin { | ||||
| } | ||||
|  | ||||
| .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 { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|  | ||||
|     input[type=submit] { | ||||
|   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 {         | ||||
|         display: flex; | ||||
|         flex-wrap: wrap; | ||||
|         align-items: center; | ||||
|         list-style-type: none; | ||||
|         margin: 0; | ||||
| @@ -81,12 +153,13 @@ nav.flexbox { | ||||
|             margin-right: 0px; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| #admin { | ||||
|     a.active { | ||||
|         font-weight: bold; | ||||
|     a { | ||||
|         text-decoration: none; | ||||
|     } | ||||
|     a.active { | ||||
|         color: #32cd32; | ||||
|         font-weight: bold; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -94,12 +167,11 @@ nav.flexbox { | ||||
|   margin: 0 auto; | ||||
|   padding: 30px 0; | ||||
|   .actor-icon { | ||||
|     width:48px; | ||||
|     width: 50px; | ||||
|     margin-right: 15px; | ||||
|     img { | ||||
|       max-width: 48px; | ||||
|     } | ||||
|     margin-top: 5px; | ||||
|   } | ||||
|  | ||||
|   .activity-content { | ||||
|     display: flex; | ||||
|     align-items:flex-start; | ||||
| @@ -112,6 +184,9 @@ nav.flexbox { | ||||
|         font-weight:normal; | ||||
|         margin-left: 5px; | ||||
|       } | ||||
|       .actor-handle { | ||||
|         color: #93a1a1; | ||||
|       } | ||||
|       .activity-date { float:right; } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
|     </select> | ||||
|     </p> | ||||
|     {% for emoji in emojis %} | ||||
|     <span class="ji">{{ emoji | emojify | safe }}</span> | ||||
|     <span class="ji">{{ emoji | emojify(True) | safe }}</span> | ||||
|     {% endfor %} | ||||
|     {% for emoji in custom_emojis %} | ||||
|     <span class="ji"><img src="{{ emoji.icon.url }}" alt="{{ emoji.name }}" title="{{ emoji.name }}" class="custom-emoji"></span> | ||||
|   | ||||
| @@ -3,8 +3,8 @@ | ||||
| <div class="h-card p-author"> | ||||
| <data class="u-photo" value="{{ local_actor.icon_url }}"></data> | ||||
| <a href="{{ local_actor.url }}" class="u-url u-uid no-hover title"> | ||||
| 	<span style="font-size:1.1em;">{{ local_actor.name }}</span> | ||||
| 	<span  style="font-size:0.85em;" class="p-name subtitle-username">{{ local_actor.handle }}</span> | ||||
| 	<span class="name">{{ local_actor.name }}</span> | ||||
| 	<span class="p-name handle">{{ local_actor.handle }}</span> | ||||
| </a> | ||||
|  | ||||
| <div class="p-note summary"> | ||||
| @@ -22,8 +22,8 @@ | ||||
| <nav class="flexbox"> | ||||
|     <ul> | ||||
|         <li>{{ header_link("index", "Notes") }}</li> | ||||
|         <li>{{ header_link("followers", "Followers") }} <span>{{ followers_count }}</span></li> | ||||
|         <li>{{ header_link("following", "Following") }} <span>{{ following_count }}</span></li> | ||||
|         <li>{{ header_link("followers", "Followers") }} <span class="counter">{{ followers_count }}</span></li> | ||||
|         <li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li> | ||||
|         <li>{{ header_link("get_remote_follow", "Remote follow") }}</li> | ||||
|     </ul> | ||||
| </nav> | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| {% extends "layout.html" %} | ||||
|  | ||||
| {% block head %} | ||||
| {% if outbox_object %} | ||||
| <link rel="alternate" href="{{ request.url }}" type="application/activity+json"> | ||||
| <meta name="description" content="{{ outbox_object.content | html2text | trim | truncate(50) }}"> | ||||
| <meta content="article" property="og:type" /> | ||||
| @@ -11,6 +12,7 @@ | ||||
| <meta content="{{ outbox_object.content | html2text | trim | truncate(50) }}" property="og:description" /> | ||||
| <meta content="{{ local_actor.icon_url }}" property="og:image" /> | ||||
| <meta content="summary" property="twitter:card" /> | ||||
| {% endif %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|   | ||||
| @@ -136,13 +136,13 @@ | ||||
|  | ||||
| {% macro display_actor(actor, actors_metadata) %} | ||||
| {% 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 style="flex: 0 0 48px;"> | ||||
|         <img src="{{ actor.resized_icon_url }}" style="max-width:45px;"> | ||||
| <div class="actor-box h-card p-author"> | ||||
|     <div class="icon-box"> | ||||
|         <img src="{{ actor.resized_icon_url }}" class="actor-icon"> | ||||
|     </div> | ||||
|     <a href="{{ actor.url }}" class="u-url" style=""> | ||||
|         <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> | ||||
| </div> | ||||
| {% if is_admin and metadata %} | ||||
| @@ -171,14 +171,13 @@ | ||||
| {% macro display_og_meta(object) %} | ||||
| {% if 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 %} | ||||
|     <div> | ||||
|     <img src="{{ og_meta.image | media_proxy_url }}" style="max-width:200px;"> | ||||
|     </div> | ||||
|     <div> | ||||
|     <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> | ||||
|     </div> | ||||
|     {% endif %} | ||||
| @@ -279,7 +278,7 @@ | ||||
|       <img src="{{ object.actor.resized_icon_url }}" alt="" class="actor-icon"> | ||||
|     <div class="activity-header"> | ||||
|         <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() }}"> | ||||
|             {{ 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> | ||||
|   | ||||
| @@ -16,6 +16,19 @@ from tests import factories | ||||
| 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( | ||||
|     db: Session, | ||||
|     client: TestClient, | ||||
|   | ||||
							
								
								
									
										13
									
								
								tests/test_tags.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								tests/test_tags.py
									
									
									
									
									
										Normal 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 | ||||
		Reference in New Issue
	
	Block a user