class PostCard(Component):
"""One post. Liking updates this card only: its state starts from the props and then
follows the server's answer, without reloading the whole timeline."""
post: PostView
can_like: bool = False
likes = State(0)
liked = State(False)
def mount(self):
self.likes, self.liked = self.post.likes, self.post.liked
async def toggle_like(self):
updated = await self.use(SocialService).toggle_like(self.post.id)
self.likes, self.liked = updated.likes, updated.liked
def render(self):
p = self.post
hearts = f"♥ {self.likes}"
count = f"{self.likes} like{'s' if self.likes != 1 else ''}"
like = (
Button(
hearts,
size="sm",
variant="secondary" if self.liked else "ghost",
aria_pressed="true" if self.liked else "false",
aria_label=f"{'Unlike' if self.liked else 'Like'} post by {p.author_name} ({count})",
on_click=self.toggle_like,
)
if self.can_like
else Text(hearts, muted=True, size="sm", aria_label=count)
)
return Card(
Row(
Avatar(p.author_name, size=40),
Column(
Row(
Link(p.author_name, href=f"/u/{p.author_handle}", style=Style(font_weight=700)),
Text(f"@{p.author_handle} · {ago(p.created_at)}", muted=True, size="sm", tag="span"),
gap="xs",
wrap=True,
),
Text(p.body, style=Style(white_space="pre-wrap", raw={"overflow-wrap": "anywhere"})),
Row(Spacer(), like),
gap="xs",
style=Style(grow=1, min_width=0),
),
align="start",
gap="sm",
),
style=PANEL,
)
Social network¶
Murmur: posts, likes, follows, profiles and a home timeline.
social-network/ ├── app.py # composition root ├── settings.py # NETWORK_NAME, MAX_POST_LENGTH, DATABASE_URL ├── models/ # database.py, member.py (Member, Follow), post.py (Post, Like) ├── services/ # social.py (SocialService), seed.py └── views/ ├── components/ # shell, composer, post_card, who_to_follow ├── pages/ # home, explore, profile, account └── styles/1. Association tables¶
Composite primary keys make “follow twice” and “like twice” impossible at the database level.
2. Timelines in a fixed number of queries¶
Authors come in one
selectinloadquery, like counts in oneGROUP BY, and your likes in oneINquery, whether the timeline has 5 posts or 500. The test suite counts the SQL statements to prove it.3. Local updates¶
Liking re-renders only that card: its state starts from the props in
mount()and then follows the server’s answer.4. A live character counter¶
The counter follows your typing and the button enables only for a valid post. The server still validates on submit, because it never trusts the browser.
5. Callbacks between components¶
Composer(posted=self.reload)andWhoToFollow(followed=self.reload)tell the page to reload the timeline.