feat: Add following and followers list

This commit is contained in:
Ro 2024-11-02 20:46:15 -05:00
parent b49925b830
commit f27dd6a9a7
Signed by: ro
GPG key ID: 5B5AD5A568CDABF9
10 changed files with 219 additions and 88 deletions

View file

@ -1,56 +1,67 @@
## Backend
## Posting
- [x] Check that signature header (digest) matches digest of body contents
- [ ] Posting
- [x] Making posts locally
- [x] Figuring out follower list
- [x] Sending to followers
- [x] Post formatting
- [ ] Sending posts w/ images / videos
- [ ] Receiving posts w/ images / videos
- [x] Timeline
- [x] My posts
- [x] Posts from accounts you follow
- [x] Show the actor avatar and display name
- [x] Making posts
- [x] Broadcasting them to followers
- [x] Post formatting
- [x] Deleting posts
- [ ] Sending posts w/ images / videos
- [ ] Making posts with CWs
- [ ] Making polls
- [ ] Followers-only posts (or maybe this is handled because we only send posts to followers? but we also include public in the TO field?)
## Profile
- [x] Profile
- [x] Name field (for display name)
- [x] Bust actor cache when you update your profile
- [x] Following
- [ ] Scrape public posts from the outbox when you follow
## Following
- [x] Sending follow request
- [x] View following list
- [ ] Withdrawing follow request
- [ ] Unfollowing
- [x] Being followed
- [x] Accepting follows
- [ ] Proactively check the outbox of newly-accepted follows
## Being Followed
- [x] Receiving follower requests
- [ ] Viewing follower requests
- [ ] Accepting follower requests
- [ ] Rejecting follower requests
- [ ] Ignoring follower requests
- [ ] Unaccepting follower request ("soft block")
- [ ] Blocking
- [ ] Approving / declining follows
- [ ] Manage authorized instance list
- [ ] Liking
- [ ] Unliking
- [ ] CW posts
- [ ] Polls
- [ ] DMs
- [ ] Support authenticated fetch of outbox (by allowed domains / servers)
- [ ] Followers-only posts (or maybe this is handled because we only send posts to followers? but we also include public in the TO field?)
## Timeline
- [x] Your posts show up in timeline
- [x] Posts from accounts you follow show up in timeline
- [x] Show the actor avatar and display name
- [ ] Receiving posts w/ images / videos
- [ ] Liking posts
- [ ] Unliking posts
- [ ] Displaying CW posts behind CW
- [ ] Displaying polls
- [ ] Voting in polls
## DMs
- [ ] Receiving DMs
- [ ] Replying to DMs
- [ ] Sending new DMs
## Allowlist
- [ ] Manage approved instance list
- [ ] Only accept activities from approved instances
- [ ] Allow approved instances to see posts in outbox
## Protocol Support
- [x] Check that signature header (digest) matches digest of body contents
- [ ] Check the domain of the public key against the domain of the object being CRUDed
## UX
- [x] Posting
- [x] Timeline
- [x] My posts
- [x] Posts from accounts you follow
- [ ] Deleting posts
- [~] Following
- [ ] Unfollowing
- [ ] Being followed
- [ ] Accepting follows
- [ ] Approving / declining follows and authorized instance list
- [ ] Liking
- [ ] Unliking
- [ ] CW posts
- [ ] Polls
- [ ] DMs
## Testing
- [ ] Measure test coverage

View file

@ -77,7 +77,7 @@ defmodule Postland.Activities do
my_actor_id = Postland.my_actor_id()
private_key = Postland.Accounts.solo_user().private_key
Postland.Follows.all_followers()
Postland.Follows.confirmed_followers()
|> Enum.map(fn %{follower: follower} ->
inbox = Postland.Actors.inbox(follower)

View file

@ -14,13 +14,20 @@ defmodule Postland.Follows do
def all_following() do
my_actor_id = Postland.my_actor_id()
from(f in Follow, where: f.follower == ^my_actor_id, where: not is_nil(f.confirmed_at))
from(f in Follow, where: f.follower == ^my_actor_id)
|> Repo.all()
end
def all_followers() do
my_actor_id = Postland.my_actor_id()
from(f in Follow, where: f.followee == ^my_actor_id)
|> Repo.all()
end
def confirmed_followers() do
my_actor_id = Postland.my_actor_id()
from(f in Follow, where: f.followee == ^my_actor_id, where: not is_nil(f.confirmed_at))
|> Repo.all()
end

View file

@ -20,6 +20,42 @@ defmodule PostlandWeb.CoreComponents do
alias Phoenix.LiveView.JS
use Gettext, backend: PostlandWeb.Gettext
def profile_card(assigns) do
%{host: host} = URI.parse(assigns.account["id"])
assigns = Map.put(assigns, :host, host)
~H"""
<div class="flex items-start space-x-4">
<div class="flex-shrink-0 relative">
<img
class="h-16 w-16 rounded-full drop-shadow-lg"
src={@account["icon"] || url(~p"/images/avatar.png")}
alt=""
/>
</div>
<div class="flex bg-white rounded-lg shadow container p-4">
<div class="py-2">
<p>
<span class="font-bold">
<%= @account["name"] %>
</span>
<span class="ml-2 text-gray-500">
@<%= @account["preferredUsername"] %>@<%= @host %>
</span>
</p>
<p class="text-gray-500 text-sm">
<%= {:safe, Earmark.as_html!(@account["summary"] || "")} %>
</p>
</div>
<div class="flex-grow text-right">
<.button disabled>Pending</.button>
</div>
</div>
</div>
"""
end
def post_form(assigns) do
user = Postland.Accounts.solo_user()
@ -121,6 +157,7 @@ defmodule PostlandWeb.CoreComponents do
phx-click="delete_post"
phx-value-post-dom-id={@post_dom_id}
phx-value-post-id={@post.id}
class="text-gray-500"
>
<.icon name="hero-trash" />
</a>

View file

@ -2,4 +2,24 @@ defmodule PostlandWeb.Layouts do
use PostlandWeb, :html
embed_templates "layouts/*"
def nav_link(assigns) do
~H"""
<li>
<!-- Current: "bg-gray-50 text-violet-600", Default: "text-gray-700 hover:text-violet-600 hover:bg-gray-50" -->
<a
href={@href}
class={[
"group flex gap-x-3 rounded-lg p-2 pl-3 text-sm/6 font-semibold",
if(assigns[:active],
do: "bg-gray-50 text-violet-600",
else: "text-gray-700 hover:text-violet-600 hover:bg-gray-50"
)
]}
>
<%= render_slot(@inner_block) %>
</a>
</li>
"""
end
end

View file

@ -1,6 +1,46 @@
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
<main class="flex items-start px-4 py-20 sm:px-6 lg:px-8">
<div :if={@current_user} class="flex-1 bg-white rounded-lg py-2 px-3 shadow">
<nav class="flex flex-col" aria-label="Sidebar">
<ul role="list" class="-mx-2 space-y-1">
<li>
<div class="group flex gap-x-3 rounded-lg p-2 pl-3 text-sm/6 uppercase font-bold">
Postland
</div>
</li>
<.nav_link href={~p"/"} active={@socket.view == PostlandWeb.TimelineLive}>
Timeline
</.nav_link>
<.nav_link href={~p"/about"} active={@socket.view == PostlandWeb.ProfileLive}>
Profile
</.nav_link>
<.nav_link href={~p"/following"} active={@socket.view == PostlandWeb.FollowingLive}>
Following
</.nav_link>
<.nav_link href={~p"/followers"} active={@socket.view == PostlandWeb.FollowersLive}>
Followers
</.nav_link>
<.nav_link
href={~p"/users/settings"}
active={@socket.view == PostlandWeb.UserSettingsLive}
>
Settings
</.nav_link>
<li>
<.link
href={~p"/users/log_out"}
method="delete"
class="group flex gap-x-3 rounded-lg p-2 pl-3 text-sm/6 font-semibold text-gray-700 hover:bg-gray-50 hover:text-violet-600"
>
Log Out
</.link>
</li>
</ul>
</nav>
</div>
<div class="flex-[5]">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>
</div>
</div>
</main>

View file

@ -12,44 +12,6 @@
</script>
</head>
<body class="bg-violet-50 antialiased">
<header class="flex w-full bg-violet-700 text-violet-100 py-4">
<div class="flex-1 px-4 uppercase font-bold ">
<a href={~p"/"}>Postland</a>
</div>
<ul class="relative z-10 flex flex-1 items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_user do %>
<li class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300">
<a href={~p"/about"}>Profile</a>
</li>
<li>
<.link
href={~p"/users/settings"}
class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300"
>
Settings
</.link>
</li>
<li>
<.link
href={~p"/users/log_out"}
method="delete"
class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300"
>
Log out
</.link>
</li>
<% else %>
<li>
<.link
href={~p"/users/log_in"}
class="text-[0.8125rem] leading-6 font-semibold hover:text-violet-300"
>
Log in
</.link>
</li>
<% end %>
</ul>
</header>
<%= @inner_content %>
</body>
</html>

View file

@ -0,0 +1,27 @@
defmodule PostlandWeb.FollowersLive do
use PostlandWeb, :live_view
alias Postland.Actors
alias Postland.Follows
def render(assigns) do
~H"""
<div class="py-4">
<h3 class="text-base font-semibold">Followers</h3>
<p :if={@accounts == []} class="text-gray-400">
No one follows you.
</p>
<div :for={acct <- @accounts} class="mt-2">
<.profile_card account={acct} />
</div>
</div>
"""
end
def mount(_params, _session, socket) do
followers =
Follows.all_followers() |> Enum.map(fn follow -> Actors.actor(follow.follower) end)
{:ok, assign(socket, :accounts, followers)}
end
end

View file

@ -0,0 +1,25 @@
defmodule PostlandWeb.FollowingLive do
use PostlandWeb, :live_view
alias Postland.Actors
alias Postland.Follows
def render(assigns) do
~H"""
<div class="py-4">
<h3 class="text-base font-semibold">Following</h3>
<p :if={@accounts == []} class="text-gray-400">
You aren't following anyone.
</p>
<div :for={acct <- @accounts} class="mt-2">
<.profile_card account={acct} />
</div>
</div>
"""
end
def mount(_params, _session, socket) do
follows = Follows.all_following() |> Enum.map(fn follow -> Actors.actor(follow.followee) end)
{:ok, assign(socket, :accounts, follows)}
end
end

View file

@ -82,6 +82,8 @@ defmodule PostlandWeb.Router do
live_session :require_authenticated_user,
on_mount: [{PostlandWeb.UserAuth, :ensure_authenticated}] do
live "/", TimelineLive, :show
live "/following", FollowingLive, :show
live "/followers", FollowersLive, :show
live "/users/settings", UserSettingsLive, :edit
live "/@:acct", OtherProfileLive, :show
end